Skip to content

refactor: improve type safety patterns from @ts-expect-error cleanup#7968

Closed
DrJKL wants to merge 28 commits intomainfrom
drjkl/lowered-expectations
Closed

refactor: improve type safety patterns from @ts-expect-error cleanup#7968
DrJKL wants to merge 28 commits intomainfrom
drjkl/lowered-expectations

Conversation

@DrJKL
Copy link
Contributor

@DrJKL DrJKL commented Jan 12, 2026

Summary

Comprehensive removal of @ts-expect-error suppressions across the codebase, replacing them with proper type guards, null checks, and type annotations.

Before: 528 @ts-expect-error suppressions in src/
After: 1 remaining (PrimeVue theme type issue in src/main.ts)

Changes by Area

GroupNode & Subgraphs

  • Removed all @ts-expect-error suppressions from groupNode.ts
  • Improved type safety with proper type guards and reduced casts
  • Created SubgraphGraph type and assertSubgraph() utility for browser tests

LiteGraph & Widgets

  • Replaced 34 @ts-expect-error suppressions in widget-related files
  • Replaced type assertions with proper type guards in litegraph code
  • Extended LGraphNode, LGraphCanvas, and LiteGraph types with missing properties
  • Added Window interface extensions for app, LiteGraph, LGraphBadge

Services & Utils

  • Removed 8 suppressions from litegraphService.ts
  • Removed 18 suppressions from various utility files
  • Used proper null guards and type narrowing

Platform & Renderer

  • Removed 12 suppressions from platform and renderer code
  • Fixed type issues in schemas, stores, and workbench

Components & Composables

  • Removed suppressions with proper type guards
  • Fixed component prop types and event handlers

Browser Tests

  • Added browser_tests/types/global.d.ts extending Window interface with test properties
  • Replaced 32 verbose (window as unknown as Record<...>)['prop'] casts with direct access
  • Added null guards to fixtures

Code Quality Improvements

  • Created WorkflowAsGraph type alias documenting ComfyWorkflowJSON → ISerialisedGraph cast
  • Simplified domWidget.ts clone pattern using getCloneArgs() factory method
  • Fixed ComponentWidgetImpl which was missing proper cloning of component-specific properties
  • Added type safety documentation to AGENTS.md

Files Changed

180 files changed, 4,168 insertions, 22,247 deletions

Key new files:

  • src/types/workflowSchemaTypes.ts - WorkflowAsGraph type alias
  • browser_tests/types/global.d.ts - Window interface extensions for tests
  • browser_tests/fixtures/utils/subgraphUtils.ts - Subgraph type guards
  • docs/typescript/type-safety.md - Type safety guidelines

Remaining Suppressions

File Reason
src/main.ts PrimeVue Aura['primitive'].blue type issue (external library)
.storybook/preview.ts Same PrimeVue issue
apps/desktop-ui/.storybook/preview.ts Same PrimeVue issue

Test Plan

  • pnpm typecheck passes
  • pnpm lint passes
  • CI tests pass

DrJKL and others added 20 commits January 11, 2026 03:09
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This PR systematically removes @ts-expect-error workarounds and hardens TypeScript typing across the frontend: it adds runtime null/undefined guards, narrows and documents types, introduces new type declarations and guards, updates several public signatures/exports, and tightens tests and test fixtures for safer runtime behavior.

Changes

Cohort / File(s) Summary
Configuration & Docs
.storybook/preview.ts, apps/desktop-ui/.storybook/preview.ts, apps/desktop-ui/src/main.ts, AGENTS.md, docs/typescript/type-safety.md
Minor comment/text updates and a new TypeScript type-safety guide.
Browser tests & fixtures
browser_tests/**, browser_tests/types/global.d.ts, browser_tests/tsconfig.json
Large test-hardening: replace bracket window['app'] access with guarded app locals, add global test typings, new subgraphUtils types/assertions, and matcher-context changes (notable signature change for toHaveFocus).
Vue components — common & dialogs
src/components/common/*, src/components/dialog/content/*, src/components/topbar/*
Replace ts-expect-error usages with optional chaining/guards, expose popover/closePopover via defineExpose; small behavioral-safe guards in input focus/selection code.
Composables, hooks & tests
src/composables/**, src/composables/**.test.ts
Stronger mocks/tests, named export for useGlobalLitegraph, new PasteableNode interface, richer typed test scaffolding; no production logic changes except useGlobalLitegraph export form.
Extensions (core)
src/extensions/core/*
Type-safety and runtime guards across extension code; new FilteredContextMenu class; group-node subsystem refactor with new types and guards (many signatures updated—review group-node public types).
LiteGraph core & types
src/lib/litegraph/src/**, src/types/*, src/types/workflowSchemaTypes.ts
Significant public-surface typing changes: new ILinkRouting, LinkSegment.data, LLink implements ILinkRouting, LGraph gains version and record-based node-state fields, LGraphNode.getInputLink now returns `LLink
UI scripts & utilities
src/scripts/**, src/scripts/ui/**, src/scripts/widgets.ts, src/utils/**
Many utility and UI modules tightened with explicit types, generics (e.g., ComfyAsyncDialog generic response type), safer DOM/event handling, and color/util signature changes (adjustColor now generic). Some function signatures/public fields changed—please scan for downstream callers.
Services & other runtime code
src/services/litegraphService.ts, src/extensions/core/webcamCapture.ts, src/extensions/core/uploadAudio.ts
Hardened media/widget flows, added typed WEBCAM_READY handling, stricter error paths; internal behavior preserved but several typed interfaces and method signatures were introduced/updated.
Tests & test infra (other)
src/**/*.test.ts, browser_tests/tests/**
Broad test updates: guards, typed helpers, new mock classes, removal of ts-expect-error in many tests. Expect updated test compilation results and some changed test helper signatures.
Minor functional/public signature changes
src/composables/useGlobalLitegraph.ts, src/composables/usePaste.ts, src/scripts/ui/components/asyncDialog.ts, src/workbench/utils/nodeDefOrderingUtil.ts
Notable signature changes: useGlobalLitegraph exported as named function, new PasteableNode interface and pasteImageNode/pasteItemsOnNode signatures, generic ComfyAsyncDialog<T> and prompt, and getOrderedInputSpecs now accepts HasInputOrder

Possibly related PRs


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DrJKL DrJKL added the claude-review Add to trigger a PR code review from Claude Code label Jan 12, 2026
@github-actions
Copy link

github-actions bot commented Jan 12, 2026

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 01/13/2026, 04:21:18 AM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@github-actions
Copy link

github-actions bot commented Jan 12, 2026

🎭 Playwright Tests: ❌ Failed

Results: 468 passed, 18 failed, 1 flaky, 8 skipped (Total: 495)

❌ Failed Tests

📊 Browser Reports
  • chromium: View Report (✅ 458 / ❌ 18 / ⚠️ 0 / ⏭️ 8)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 7 / ❌ 0 / ⚠️ 1 / ⏭️ 0)

@DrJKL
Copy link
Contributor Author

DrJKL commented Jan 12, 2026

@coderabbitai review full

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

@DrJKL: I'll conduct a full review of this PR. Given the comprehensive nature of this type safety refactoring (180 files, removing 527 @ts-expect-error suppressions), I'll thoroughly examine:

  • Type guard implementations replacing suppressions
  • New type definitions and extensions
  • Browser test type improvements
  • The remaining PrimeVue suppression
  • Overall type safety improvements

Starting the full review now.


🧠 Learnings used
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} : Avoid using ts-expect-error; use proper TypeScript types instead

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/**/*.{ts,tsx} : Type assertions are an absolute last resort. In almost all cases, they are a crutch that leads to brittle code

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} : Implement proper TypeScript types throughout the codebase

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/**/*.ts : Use TypeScript for type safety

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T00:24:17.695Z
Learning: Applies to src/**/*.{ts,tsx,vue} : Never use `as any` type assertions - fix the underlying type issue

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

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7898
File: src/composables/usePaste.test.ts:248-248
Timestamp: 2026-01-09T02:07:59.035Z
Learning: In test files at src/**/*.test.ts, when creating mock objects that partially implement an interface (e.g., LGraphNode), use `as Partial<InterfaceType> as InterfaceType` instead of `as any` or `as unknown as InterfaceType` to explicitly acknowledge the incomplete implementation while maintaining type safety.

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

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T00:24:17.695Z
Learning: Applies to +(tests-ui|src)/**/*.test.ts : Write tests for all changes, especially bug fixes to catch future regressions

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} : Use TypeScript for type safety in state management stores
✅ Actions performed

Full review triggered.

@github-actions
Copy link

github-actions bot commented Jan 12, 2026

Bundle Size Report

Summary

  • Raw size: 19.4 MB baseline 19.4 MB — 🔴 +9.01 kB
  • Gzip: 3.96 MB baseline 3.95 MB — 🔴 +972 B
  • Brotli: 3.02 MB baseline 3.02 MB — 🔴 +2.33 kB
  • Bundles: 98 current • 98 baseline • 51 added / 51 removed

Category Glance
App Entry Points 🔴 +6.89 kB (3.33 MB) · Graph Workspace 🔴 +1.05 kB (1.04 MB) · Vendor & Third-Party 🔴 +874 B (9.19 MB) · Other 🔴 +132 B (5.25 MB) · Panels & Settings 🔴 +55 B (372 kB) · UI Components 🔴 +6 B (200 kB) · + 3 more

Per-category breakdown
App Entry Points — 3.33 MB (baseline 3.32 MB) • 🔴 +6.89 kB

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-rmjwHAYw.js (new) 3.12 MB 🔴 +3.12 MB 🔴 +655 kB 🔴 +497 kB
assets/index-C_pmfadn.js (removed) 3.12 MB 🟢 -3.12 MB 🟢 -654 kB 🟢 -496 kB
assets/index-B4W3qJrT.js (new) 204 kB 🔴 +204 kB 🔴 +44.8 kB 🔴 +37 kB
assets/index-TpI5dvzF.js (removed) 200 kB 🟢 -200 kB 🟢 -44 kB 🟢 -36.4 kB
assets/index-4SeCsy6M.js (removed) 345 B 🟢 -345 B 🟢 -243 B 🟢 -210 B
assets/index-x16Wdc9y.js (new) 345 B 🔴 +345 B 🔴 +245 B 🔴 +234 B

Status: 3 added / 3 removed

Graph Workspace — 1.04 MB (baseline 1.04 MB) • 🔴 +1.05 kB

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-CRZy-EzQ.js (new) 1.04 MB 🔴 +1.04 MB 🔴 +200 kB 🔴 +152 kB
assets/GraphView-DoybSy1a.js (removed) 1.04 MB 🟢 -1.04 MB 🟢 -200 kB 🟢 -151 kB

Status: 1 added / 1 removed

Views & Navigation — 6.63 kB (baseline 6.63 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/UserSelectView-CjIoJQr6.js (removed) 6.63 kB 🟢 -6.63 kB 🟢 -2.14 kB 🟢 -1.9 kB
assets/UserSelectView-CZvoap7R.js (new) 6.63 kB 🔴 +6.63 kB 🔴 +2.14 kB 🔴 +1.9 kB

Status: 1 added / 1 removed

Panels & Settings — 372 kB (baseline 372 kB) • 🔴 +55 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/LegacyCreditsPanel-3lBFnv6R.js (removed) 25.1 kB 🟢 -25.1 kB 🟢 -5.74 kB 🟢 -5 kB
assets/LegacyCreditsPanel-Ba5G_XR7.js (new) 25.1 kB 🔴 +25.1 kB 🔴 +5.74 kB 🔴 +5 kB
assets/KeybindingPanel-oyEHv1ha.js (new) 14.9 kB 🔴 +14.9 kB 🔴 +3.59 kB 🔴 +3.14 kB
assets/KeybindingPanel-DrNf_zME.js (removed) 14.8 kB 🟢 -14.8 kB 🟢 -3.57 kB 🟢 -3.13 kB
assets/ExtensionPanel-B4ZOD55_.js (new) 11.1 kB 🔴 +11.1 kB 🔴 +2.62 kB 🔴 +2.29 kB
assets/ExtensionPanel-Buv-MVGG.js (removed) 11.1 kB 🟢 -11.1 kB 🟢 -2.62 kB 🟢 -2.3 kB
assets/AboutPanel-cBsAtc0i.js (new) 9.16 kB 🔴 +9.16 kB 🔴 +2.46 kB 🔴 +2.21 kB
assets/AboutPanel-dLhmQSjQ.js (removed) 9.16 kB 🟢 -9.16 kB 🟢 -2.46 kB 🟢 -2.21 kB
assets/ServerConfigPanel-Mz_z39Dj.js (removed) 7.51 kB 🟢 -7.51 kB 🟢 -2.04 kB 🟢 -1.8 kB
assets/ServerConfigPanel-R2p-Rh1p.js (new) 7.51 kB 🔴 +7.51 kB 🔴 +2.04 kB 🔴 +1.8 kB
assets/UserPanel-B2IHkXWw.js (removed) 6.88 kB 🟢 -6.88 kB 🟢 -1.79 kB 🟢 -1.56 kB
assets/UserPanel-CacsbERz.js (new) 6.88 kB 🔴 +6.88 kB 🔴 +1.79 kB 🔴 +1.57 kB
assets/settings-BGQfQzTx.js 25.6 kB 25.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BVE4KHTw.js 22.7 kB 22.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BVtpJmlU.js 30.9 kB 30.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-C2aO00Dz.js 28.6 kB 28.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-Cm3ieBXR.js 27.8 kB 27.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CzQKMdK3.js 26.2 kB 26.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CzYUIUnL.js 27.1 kB 27.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DwKpL7jw.js 26.3 kB 26.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DX8feV4n.js 25.3 kB 25.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-mWzYycGc.js 22 kB 22 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-U4AdZ8Rl.js 34.9 kB 34.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 6 added / 6 removed

UI Components — 200 kB (baseline 200 kB) • 🔴 +6 B

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/LazyImage.vue_vue_type_script_setup_true_lang-C-vq6mtq.js (new) 65.6 kB 🔴 +65.6 kB 🔴 +13.3 kB 🔴 +11.5 kB
assets/LazyImage.vue_vue_type_script_setup_true_lang-D792ZMOc.js (removed) 65.6 kB 🟢 -65.6 kB 🟢 -13.3 kB 🟢 -11.5 kB
assets/Load3D.vue_vue_type_script_setup_true_lang-M9uaX_fI.js (new) 56.4 kB 🔴 +56.4 kB 🔴 +8.78 kB 🔴 +7.54 kB
assets/Load3D.vue_vue_type_script_setup_true_lang-DoOz8Xre.js (removed) 56.4 kB 🟢 -56.4 kB 🟢 -8.77 kB 🟢 -7.53 kB
assets/WidgetSelect.vue_vue_type_script_setup_true_lang-abdEj4ih.js (new) 49 kB 🔴 +49 kB 🔴 +10.5 kB 🔴 +9.15 kB
assets/WidgetSelect.vue_vue_type_script_setup_true_lang-H2K2vUgy.js (removed) 49 kB 🟢 -49 kB 🟢 -10.5 kB 🟢 -9.14 kB
assets/WidgetInputNumber.vue_vue_type_script_setup_true_lang-C4vD0WYN.js (removed) 10.9 kB 🟢 -10.9 kB 🟢 -2.89 kB 🟢 -2.56 kB
assets/WidgetInputNumber.vue_vue_type_script_setup_true_lang-Cy6zdRcM.js (new) 10.9 kB 🔴 +10.9 kB 🔴 +2.89 kB 🔴 +2.55 kB
assets/ComfyQueueButton-DZysR4b7.js (removed) 8.83 kB 🟢 -8.83 kB 🟢 -2.58 kB 🟢 -2.3 kB
assets/ComfyQueueButton-K7cWt2JK.js (new) 8.83 kB 🔴 +8.83 kB 🔴 +2.58 kB 🔴 +2.3 kB
assets/WidgetWithControl.vue_vue_type_script_setup_true_lang-CXbL_bzJ.js (new) 3.72 kB 🔴 +3.72 kB 🔴 +1.46 kB 🔴 +1.31 kB
assets/WidgetWithControl.vue_vue_type_script_setup_true_lang-DTmR7chl.js (removed) 3.72 kB 🟢 -3.72 kB 🟢 -1.45 kB 🟢 -1.31 kB
assets/WidgetButton-ByqFNFej.js (removed) 2.21 kB 🟢 -2.21 kB 🟢 -996 B 🟢 -896 B
assets/WidgetButton-DO1547uL.js (new) 2.21 kB 🔴 +2.21 kB 🔴 +997 B 🔴 +870 B
assets/WidgetLayoutField.vue_vue_type_script_setup_true_lang-D3Uf51Ue.js (removed) 2.14 kB 🟢 -2.14 kB 🟢 -888 B 🟢 -763 B
assets/WidgetLayoutField.vue_vue_type_script_setup_true_lang-tzU50MeZ.js (new) 2.14 kB 🔴 +2.14 kB 🔴 +890 B 🔴 +775 B
assets/UserAvatar.vue_vue_type_script_setup_true_lang-BELEllpm.js (new) 1.34 kB 🔴 +1.34 kB 🔴 +686 B 🔴 +594 B
assets/UserAvatar.vue_vue_type_script_setup_true_lang-DWEfpRJs.js (removed) 1.34 kB 🟢 -1.34 kB 🟢 -688 B 🟢 -598 B

Status: 9 added / 9 removed

Data & Services — 12.5 kB (baseline 12.5 kB) • ⚪ 0 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/keybindingService-B3SJAWc3.js (removed) 7.51 kB 🟢 -7.51 kB 🟢 -1.83 kB 🟢 -1.57 kB
assets/keybindingService-CVPD05Vf.js (new) 7.51 kB 🔴 +7.51 kB 🔴 +1.83 kB 🔴 +1.58 kB
assets/serverConfigStore-CR6tx5bM.js (new) 2.83 kB 🔴 +2.83 kB 🔴 +907 B 🔴 +793 B
assets/serverConfigStore-lZrt20fn.js (removed) 2.83 kB 🟢 -2.83 kB 🟢 -906 B 🟢 -788 B
assets/audioService-3Pzp5dA-.js (new) 2.2 kB 🔴 +2.2 kB 🔴 +961 B 🔴 +820 B
assets/audioService-BraVis5C.js (removed) 2.2 kB 🟢 -2.2 kB 🟢 -960 B 🟢 -823 B

Status: 3 added / 3 removed

Utilities & Hooks — 1.41 kB (baseline 1.41 kB) • ⚪ 0 B

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/audioUtils-B8YIVoVM.js (removed) 1.41 kB 🟢 -1.41 kB 🟢 -649 B 🟢 -547 B
assets/audioUtils-PMtozhJI.js (new) 1.41 kB 🔴 +1.41 kB 🔴 +651 B 🔴 +547 B

Status: 1 added / 1 removed

Vendor & Third-Party — 9.19 MB (baseline 9.19 MB) • 🔴 +874 B

External libraries and shared vendor chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/vendor-other-dkLLb5u2.js (new) 3.9 MB 🔴 +3.9 MB 🔴 +847 kB 🔴 +679 kB
assets/vendor-other-Da7BzmIq.js (removed) 3.9 MB 🟢 -3.9 MB 🟢 -848 kB 🟢 -678 kB
assets/vendor-three-Ds3gPtNh.js (removed) 2.08 MB 🟢 -2.08 MB 🟢 -430 kB 🟢 -308 kB
assets/vendor-three-LppbQOdM.js (new) 2.08 MB 🔴 +2.08 MB 🔴 +430 kB 🔴 +308 kB
assets/vendor-primevue-CvitJUGk.js (removed) 1.95 MB 🟢 -1.95 MB 🟢 -333 kB 🟢 -201 kB
assets/vendor-primevue-SBiS8kfJ.js (new) 1.95 MB 🔴 +1.95 MB 🔴 +333 kB 🔴 +201 kB
assets/vendor-chart-C2WamoVK.js (removed) 452 kB 🟢 -452 kB 🟢 -99 kB 🟢 -81 kB
assets/vendor-chart-C37t4CDG.js (new) 452 kB 🔴 +452 kB 🔴 +99 kB 🔴 +81 kB
assets/vendor-tiptap-IowFKcHG.js (new) 232 kB 🔴 +232 kB 🔴 +45.7 kB 🔴 +37.7 kB
assets/vendor-tiptap-WJL3cqV8.js (removed) 232 kB 🟢 -232 kB 🟢 -45.7 kB 🟢 -37.7 kB
assets/vendor-vue-CvLF_5-9.js (new) 160 kB 🔴 +160 kB 🔴 +37 kB 🔴 +31.4 kB
assets/vendor-vue-E9hBZNUh.js (removed) 160 kB 🟢 -160 kB 🟢 -37 kB 🟢 -31.3 kB
assets/vendor-xterm-BF8peZ5_.js 420 kB 420 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 6 added / 6 removed

Other — 5.25 MB (baseline 5.25 MB) • 🔴 +132 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/SubscriptionRequiredDialogContent-CltlIkmE.js (new) 29.3 kB 🔴 +29.3 kB 🔴 +6.51 kB 🔴 +5.66 kB
assets/SubscriptionRequiredDialogContent-M_A-XXYH.js (removed) 29.3 kB 🟢 -29.3 kB 🟢 -6.51 kB 🟢 -5.65 kB
assets/WidgetRecordAudio-BdDlqpnP.js (new) 20.4 kB 🔴 +20.4 kB 🔴 +5.24 kB 🔴 +4.63 kB
assets/WidgetRecordAudio-CwSw9T8i.js (removed) 20.4 kB 🟢 -20.4 kB 🟢 -5.23 kB 🟢 -4.63 kB
assets/AudioPreviewPlayer-BfhY4Rdb.js (removed) 13.3 kB 🟢 -13.3 kB 🟢 -3.35 kB 🟢 -3 kB
assets/AudioPreviewPlayer-CyP8AYmL.js (new) 13.3 kB 🔴 +13.3 kB 🔴 +3.35 kB 🔴 +3 kB
assets/ValueControlPopover-CBiaviuB.js (new) 5.49 kB 🔴 +5.49 kB 🔴 +1.71 kB 🔴 +1.51 kB
assets/ValueControlPopover-DgkKN2gJ.js (removed) 5.49 kB 🟢 -5.49 kB 🟢 -1.71 kB 🟢 -1.52 kB
assets/WidgetGalleria-C3KsKIom.js (new) 4.1 kB 🔴 +4.1 kB 🔴 +1.45 kB 🔴 +1.31 kB
assets/WidgetGalleria-tA5xhXQt.js (removed) 4.1 kB 🟢 -4.1 kB 🟢 -1.45 kB 🟢 -1.3 kB
assets/WidgetColorPicker-B8T8e_2V.js (removed) 3.41 kB 🟢 -3.41 kB 🟢 -1.38 kB 🟢 -1.24 kB
assets/WidgetColorPicker-CdN5jdAb.js (new) 3.41 kB 🔴 +3.41 kB 🔴 +1.38 kB 🔴 +1.23 kB
assets/WidgetImageCompare-DOYclIRS.js (removed) 3.21 kB 🟢 -3.21 kB 🟢 -1.1 kB 🟢 -960 B
assets/WidgetImageCompare-DZqZutaN.js (new) 3.21 kB 🔴 +3.21 kB 🔴 +1.1 kB 🔴 +963 B
assets/WidgetMarkdown-CKFlrgp8.js (new) 3.21 kB 🔴 +3.21 kB 🔴 +1.32 kB 🔴 +1.16 kB
assets/WidgetTextarea-DMKM_iYm.js (new) 3.08 kB 🔴 +3.08 kB 🔴 +1.22 kB 🔴 +1.08 kB
assets/WidgetTextarea-jQGFybBJ.js (removed) 3.08 kB 🟢 -3.08 kB 🟢 -1.21 kB 🟢 -1.07 kB
assets/WidgetMarkdown-CHrzUGG8.js (removed) 3.08 kB 🟢 -3.08 kB 🟢 -1.28 kB 🟢 -1.12 kB
assets/WidgetAudioUI-BJnxSGiV.js (removed) 2.89 kB 🟢 -2.89 kB 🟢 -1.17 kB 🟢 -1.06 kB
assets/WidgetAudioUI-ClzWXpTD.js (new) 2.89 kB 🔴 +2.89 kB 🔴 +1.17 kB 🔴 +1.06 kB
assets/WidgetToggleSwitch-BG5i256m.js (new) 2.66 kB 🔴 +2.66 kB 🔴 +1.13 kB 🔴 +1.02 kB
assets/WidgetToggleSwitch-CcCsa_34.js (removed) 2.66 kB 🟢 -2.66 kB 🟢 -1.13 kB 🟢 -1.02 kB
assets/MediaVideoTop-euoHxxM5.js (removed) 2.65 kB 🟢 -2.65 kB 🟢 -1.01 kB 🟢 -868 B
assets/MediaVideoTop-guYrz57W.js (new) 2.65 kB 🔴 +2.65 kB 🔴 +1.02 kB 🔴 +868 B
assets/WidgetChart-515bxB6A.js (removed) 2.48 kB 🟢 -2.48 kB 🟢 -934 B 🟢 -816 B
assets/WidgetChart-C0QbTeNn.js (new) 2.48 kB 🔴 +2.48 kB 🔴 +932 B 🔴 +817 B
assets/WidgetInputText-C0hWf9Ig.js (new) 1.99 kB 🔴 +1.99 kB 🔴 +922 B 🔴 +855 B
assets/WidgetInputText-on8Nn6nP.js (removed) 1.99 kB 🟢 -1.99 kB 🟢 -919 B 🟢 -857 B
assets/MediaImageTop-C_Y6bkH_.js (new) 1.75 kB 🔴 +1.75 kB 🔴 +841 B 🔴 +713 B
assets/MediaImageTop-C3CvQRZ8.js (removed) 1.75 kB 🟢 -1.75 kB 🟢 -843 B 🟢 -716 B
assets/Media3DTop--7xua3zf.js (removed) 1.49 kB 🟢 -1.49 kB 🟢 -764 B 🟢 -650 B
assets/Media3DTop-DsSAvjva.js (new) 1.49 kB 🔴 +1.49 kB 🔴 +768 B 🔴 +653 B
assets/MediaAudioTop-DIQlZoPQ.js (removed) 1.46 kB 🟢 -1.46 kB 🟢 -738 B 🟢 -619 B
assets/MediaAudioTop-L3QjaDni.js (new) 1.46 kB 🔴 +1.46 kB 🔴 +736 B 🔴 +616 B
assets/WidgetSelect-C3CNJugk.js (new) 733 B 🔴 +733 B 🔴 +362 B 🔴 +326 B
assets/WidgetSelect-ElD4CR_k.js (removed) 733 B 🟢 -733 B 🟢 -360 B 🟢 -326 B
assets/WidgetInputNumber-D09mlIED.js (new) 673 B 🔴 +673 B 🔴 +349 B 🔴 +290 B
assets/WidgetInputNumber-Xrhxy1o3.js (removed) 673 B 🟢 -673 B 🟢 -347 B 🟢 -287 B
assets/Load3D-_QMeHL_z.js (new) 424 B 🔴 +424 B 🔴 +266 B 🔴 +221 B
assets/Load3D-DCkNd1eN.js (removed) 424 B 🟢 -424 B 🟢 -266 B 🟢 -223 B
assets/WidgetLegacy-_xb7howl.js (removed) 364 B 🟢 -364 B 🟢 -238 B 🟢 -192 B
assets/WidgetLegacy-CHp4Hbg5.js (new) 364 B 🔴 +364 B 🔴 +237 B 🔴 +194 B
assets/commands-7wafkPd1.js 15.6 kB 15.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-B3CjBnJe.js 14.6 kB 14.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-Bbu2TJPy.js 18.1 kB 18.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BeJss6Df.js 16.9 kB 16.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-C4khqsmz.js 15.6 kB 15.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CDQ7KnIn.js 16.4 kB 16.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CtjMsXHh.js 15.4 kB 15.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CW_l758_.js 16.1 kB 16.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DbX4HUlI.js 15.6 kB 15.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DiGLEtFT.js 16.9 kB 16.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-vRVJOWju.js 14.8 kB 14.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-B_h9YBFl.js 129 kB 129 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-BO2lyoih.js 94.2 kB 94.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-BWknJp3D.js 133 kB 133 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-C49hGWeZ.js 107 kB 107 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-CgivmeTy.js 110 kB 110 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-D_QQCDTL.js 149 kB 149 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-dbu9MIPO.js 123 kB 123 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DhgCCEQx.js 113 kB 113 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DpPNoJth.js 106 kB 106 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-kIdEUfEK.js 94.9 kB 94.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-xSVehrCG.js 108 kB 108 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-B-XzzBeS.js 317 kB 317 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-BGwoeek4.js 329 kB 329 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-C6xl5-mL.js 358 kB 358 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CRZGOJB7.js 310 kB 310 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-D8-Yzlzh.js 289 kB 289 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-Du8VrAwA.js 320 kB 320 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-jjlLVrIs.js 317 kB 317 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-JQwk1kgy.js 292 kB 292 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-JuuXdMpv.js 391 kB 391 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-l2Y20bod.js 314 kB 314 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-VLMdhOwo.js 357 kB 357 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/OBJLoader2WorkerModule-DTMpvldF.js 109 kB 109 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetPropFilter-BIbGSUAt.js 1.28 kB 1.28 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 21 added / 21 removed

@github-actions github-actions bot removed the claude-review Add to trigger a PR code review from Claude Code label Jan 12, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 68

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (34)
src/lib/litegraph/src/polyfills.ts (1)

92-93: Bug: Incorrect radius variable used for bottom-left corner.

Line 92 uses bottom_right_radius when drawing the bottom edge to the bottom-left corner. This should be bottom_left_radius to correctly position the start of the bottom-left curve when corners have different radii.

🐛 Proposed fix
       // bottom left
-      this.lineTo(x + bottom_right_radius, y + h)
+      this.lineTo(x + bottom_left_radius, y + h)
       this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius)
src/scripts/ui/imagePreview.ts (1)

22-25: Guard against empty array to prevent runtime error.

Accessing imgs[0] without checking array length will throw if imgs is empty, as undefined.naturalWidth is a TypeError.

🛠️ Suggested fix
 export function calculateImageGrid(
   imgs: ImageLike[],
   dw: number,
   dh: number
 ): {
   cellWidth: number
   cellHeight: number
   cols: number
   rows: number
   shiftX: number
 } {
+  if (imgs.length === 0) {
+    return { cellWidth: 0, cellHeight: 0, cols: 0, rows: 0, shiftX: 0 }
+  }
+
   let best = 0
   let w = imgs[0].naturalWidth
   let h = imgs[0].naturalHeight
src/composables/maskeditor/useCanvasManager.test.ts (1)

127-145: Consider adding explicit style objects to imgCanvas and rgbCanvas for consistency.

While imgCanvas and rgbCanvas currently have empty style: {} objects (lines 130, 145) and maskCanvas has explicit style properties, this inconsistency could cause issues if the implementation later accesses style properties on these canvases. Consider initializing them consistently:

♻️ Optional: Initialize all canvas style objects consistently
     mockStore.imgCanvas = {
       width: 0,
       height: 0,
-      style: {}
+      style: {
+        mixBlendMode: '',
+        opacity: '',
+        backgroundColor: ''
+      }
     }

     mockStore.maskCanvas = {
       width: 0,
       height: 0,
       style: {
         mixBlendMode: '',
-        opacity: ''
+        opacity: '',
+        backgroundColor: ''
       }
     }

     mockStore.rgbCanvas = {
       width: 0,
       height: 0,
-      style: {}
+      style: {
+        mixBlendMode: '',
+        opacity: '',
+        backgroundColor: ''
+      }
     }
src/scripts/ui/dialog.ts (1)

37-44: Sanitize HTML input to prevent XSS vulnerabilities.

The innerHTML assignment at line 39 accepts unsanitized string input, creating an XSS vulnerability. Per coding guidelines, HTML content must be sanitized with DOMPurify.

🔒 Proposed fix using DOMPurify

Add DOMPurify import at the top of the file:

import DOMPurify from 'dompurify'

Then sanitize the HTML string:

   show(html?: string | HTMLElement | HTMLElement[]) {
     if (typeof html === 'string') {
-      this.textElement.innerHTML = html
+      this.textElement.innerHTML = DOMPurify.sanitize(html)
     } else if (html) {
src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts (2)

324-337: Consider Partial<LGraphNode> cast for clearer intent.

Per repository learnings, as Partial<LGraphNode> as LGraphNode is preferred over as unknown as LGraphNode when creating partial mock objects. This makes the incomplete implementation explicit while maintaining type safety.

♻️ Optional: Use Partial pattern
   const createMockNode = (type: string, packId?: string, version?: string) =>
     ({
       type,
       properties: { cnr_id: packId, ver: version },
       id: 1,
       title: type,
       pos: [0, 0],
       size: [100, 100],
       flags: {},
       graph: null,
       mode: 0,
       inputs: [],
       outputs: []
-    }) as unknown as LGraphNode
+    }) as Partial<LGraphNode> as LGraphNode

Verify whether Partial<LGraphNode> compiles cleanly given the nested properties object structure. If TypeScript complains about required nested properties, the current as unknown as pattern is acceptable.


406-423: Duplicate helper function; consider extracting to shared scope.

This createMockNode function duplicates the one at lines 324-337. Extract it to the top-level describe block or module scope to avoid redundancy.

♻️ Extract shared helper

Move the helper function outside the nested describe blocks (e.g., after line 93 near mockWorkflowPacks) so both test sections can reuse it:

const createMockNode = (
  type: string,
  packId?: string,
  version?: string
): LGraphNode =>
  ({
    type,
    properties: { cnr_id: packId, ver: version },
    id: 1,
    title: type,
    pos: [0, 0],
    size: [100, 100],
    flags: {},
    graph: null,
    mode: 0,
    inputs: [],
    outputs: []
  }) as Partial<LGraphNode> as LGraphNode

Then remove both local definitions.

src/workbench/utils/nodeDefOrderingUtil.ts (1)

89-94: TypeScript doesn't narrow Map.get() after Map.has() check.

The valueMap.get(name) return type remains TWidgetValue | undefined even after the has() check, potentially allowing undefined into the array.

♻️ Option 1: Use non-null assertion (safe here due to prior has() check)
   for (const name of inputOrder) {
     if (valueMap.has(name)) {
-      reordered.push(valueMap.get(name))
+      reordered.push(valueMap.get(name)!)
       usedNames.add(name)
     }
   }
♻️ Option 2: Single get() with type guard (changes behavior if undefined is valid TWidgetValue)
   for (const name of inputOrder) {
-    if (valueMap.has(name)) {
-      reordered.push(valueMap.get(name))
+    const value = valueMap.get(name)
+    if (value !== undefined) {
+      reordered.push(value)
       usedNames.add(name)
     }
   }

Option 1 is preferred if TWidgetValue can legitimately include undefined as a valid value.

src/scripts/ui/toggleSwitch.ts (1)

40-64: Side effect: mutating caller's objects.

Line 42 mutates the original item object when it's not a string, which could affect the caller's data. This is pre-existing behavior but worth noting. The proposed fix in the previous comment (creating normalizedItems) would also address this by not mutating the originals.

src/scripts/widgets.ts (1)

193-230: Consider caching original values to avoid redundant computation.

getComboValuesArray is called twice when the filter removes all items - once for the filtered values and once for the warning check. Since the helper may invoke a function (line 24), this could have side effects or performance implications.

♻️ Suggested optimization
     if (isCombo && v !== 'fixed') {
       const comboWidget = targetWidget as IComboWidget
-      let values = getComboValuesArray(
+      const originalValues = getComboValuesArray(
         comboWidget.options.values,
         comboWidget,
         node
       )
+      let values = originalValues
       const filter = comboFilter?.value
       if (filter) {
         let check: ((item: string) => boolean) | undefined
         // ... regex/check logic unchanged ...
         values = values.filter(check)
-        if (!values.length && comboWidget.options.values) {
-          const originalValues = getComboValuesArray(
-            comboWidget.options.values,
-            comboWidget,
-            node
-          )
-          if (originalValues.length) {
+        if (!values.length && originalValues.length) {
             console.warn(
               'Filter for node ' + node.id + ' has filtered out all items',
               filter
             )
-          }
         }
       }
src/composables/graph/useSelectionState.test.ts (1)

157-179: Misleading test name: "should provide non-reactive state computation".

The test name suggests testing non-reactive behavior, but it actually verifies that a new useSelectionState() instance correctly reflects the current (empty) store state after clearing selectedItems. This is testing that the composable reads from the shared store correctly, not non-reactivity.

Consider renaming to something like "should reflect current store state in new composable instance" to better describe the actual behavior being tested.

browser_tests/fixtures/utils/taskHistory.ts (1)

119-140: Type annotation improves safety; consider using TaskOutput directly.

The explicit accumulator type is a good improvement. Since TaskOutput is already imported (line 10), consider using it directly to avoid type drift:

   private createOutputs(
     filenames: string[],
     filetype: OutputFileType
   ): TaskOutput {
     return filenames.reduce(
       (outputs, filename, i) => {
         const nodeId = `${i + 1}`
         outputs[nodeId] = {
           [filetype]: [{ filename, subfolder: '', type: 'output' }]
         }
         const contentType = getContentType(filename, filetype)
         this.outputContentTypes.set(filename, contentType)
         return outputs
       },
-      {} as Record<
-        string,
-        {
-          [key: string]: { filename: string; subfolder: string; type: string }[]
-        }
-      >
+      {} as TaskOutput
     )
   }
src/scripts/metadata/png.ts (1)

45-55: Promise may never settle if type guard fails.

The instanceof ArrayBuffer guard is correct for type narrowing, but if the condition fails, the promise never resolves or rejects. This could cause the caller to hang indefinitely.

Proposed fix to ensure the promise always settles
 export function getFromPngFile(file: File) {
   return new Promise<Record<string, string>>((r) => {
     const reader = new FileReader()
     reader.onload = (event) => {
       if (event.target?.result instanceof ArrayBuffer) {
         r(getFromPngBuffer(event.target.result))
+      } else {
+        r({})
       }
     }
+    reader.onerror = () => r({})

     reader.readAsArrayBuffer(file)
   })
 }

This ensures consistent behavior with the existing getFromPngBuffer function which returns {} on invalid input.

src/components/dialog/content/MissingCoreNodesMessage.test.ts (1)

17-31: Consider using Partial<LGraphNode> pattern for mock nodes.

Based on learnings, when creating mock objects that partially implement an interface, prefer as Partial<LGraphNode> as LGraphNode over Object.create(null). This explicitly acknowledges the incomplete implementation while maintaining type safety.

♻️ Suggested refactor
 function createMockNode(type: string, version?: string): LGraphNode {
-  return Object.assign(Object.create(null), {
+  return {
     type,
     properties: { cnr_id: 'comfy-core', ver: version },
     id: 1,
     title: type,
     pos: [0, 0],
     size: [100, 100],
     flags: {},
     graph: null,
     mode: 0,
     inputs: [],
     outputs: []
-  })
+  } as Partial<LGraphNode> as LGraphNode
 }
browser_tests/tests/featureFlags.spec.ts (1)

332-373: Consider typing __appReadiness instead of using as any.

Lines 334, 346, 354, 362, and 393 use (window as any).__appReadiness. For consistency with the rest of the file's type improvements, consider adding __appReadiness to the module-level window augmentation (already present on lines 11-15) and using typed access inside the callback.

♻️ Suggested improvement
      // Track when various app components are ready
-      ;(window as any).__appReadiness = {
+      const win = window as Window & typeof globalThis & {
+        __appReadiness: {
+          featureFlagsReceived: boolean
+          apiInitialized: boolean
+          appInitialized: boolean
+        }
+      }
+      win.__appReadiness = {
         featureFlagsReceived: false,
         apiInitialized: false,
         appInitialized: false
       }
src/lib/litegraph/src/LGraphCanvas.ts (6)

106-120: Fix showSearchBox options mismatch (slot_to is ignored at runtime).
IShowSearchOptions only supports slot_from, but linkReleaseContext builds { slot_to: ... } in the “output” branch and then casts to IShowSearchOptions (Line 850). showSearchBox() later only reads options.slot_from, so the “output” path won’t pre-select/connect correctly.

Proposed fix
-          const linkReleaseContext =
+          const linkReleaseContext: IShowSearchOptions =
             this.linkConnector.state.connectingTo === 'input'
               ? {
                   node_from: firstLink.node as LGraphNode,
                   slot_from: firstLink.fromSlot as INodeOutputSlot,
                   type_filter_in: firstLink.fromSlot.type
                 }
               : {
                   node_to: firstLink.node as LGraphNode,
-                  slot_to: firstLink.fromSlot as INodeInputSlot,
+                  slot_from: firstLink.fromSlot as INodeInputSlot,
                   type_filter_out: firstLink.fromSlot.type
                 }
@@
-              this.showSearchBox(e, linkReleaseContext as IShowSearchOptions)
+              this.showSearchBox(e, linkReleaseContext)
-->

Also applies to: 850-851


1399-1499: onShowPropertyEditor: non-title edits are written to node.properties[...] (likely wrong).
The new logic writes any keyof LGraphNode (except 'title') into node.properties[property as string] (Line 1496-1499). That changes semantics if this editor is ever used for real node fields (e.g. mode, pos, size, etc.).

If this menu is truly “Title only”, restrict it to property === 'title' and remove the generic branch (safer than silently corrupting node.properties).
-->


1517-1523: Safer enum lookup: avoid iterating inherited keys.
for (const k in valuesRecord) can walk prototype keys (Line 1518). Prefer Object.entries(values as Record<string, unknown>) or guard with hasOwnProperty.
-->


3681-3722: Keyboard shortcuts still fire while typing in <textarea> / contenteditable.
You now early-return only for HTMLInputElement (Line 3682). That means space/escape/etc can still trigger canvas behaviors while focused in a textarea (or other editable elements), even though you later special-case Delete/Backspace.

Proposed fix
-    const targetEl = e.target
-    if (targetEl instanceof HTMLInputElement) return
+    const targetEl = e.target
+    if (
+      targetEl instanceof HTMLInputElement ||
+      targetEl instanceof HTMLTextAreaElement ||
+      (targetEl instanceof HTMLElement && targetEl.isContentEditable)
+    )
+      return
-->

7448-7530: showEditPropertyValue: boolean checkbox writes strings; array/object parse can throw on undefined.

  • Checkbox path calls setValue('true'|'false') (Line 7456-7458), but setValue doesn’t convert to boolean, so you’ll persist a string.
  • For type === 'array'|'object', JSON.parse(String(value)) (Line 7520-7521) will throw if value is undefined (e.g. input missing or cleared), crashing the interaction.
Proposed fix
-      if (input instanceof HTMLInputElement) {
+      if (input instanceof HTMLInputElement) {
         const checkbox = input
         checkbox.addEventListener('click', function () {
           dialog.modified()
-          // Convert boolean to string for setValue which expects string
-          setValue(checkbox.checked ? 'true' : 'false')
+          setValue(checkbox.checked)
         })
       }
@@
-    function setValue(value: string | number | undefined) {
+    function setValue(value: unknown) {
       if (
-        value !== undefined &&
+        value !== undefined &&
         info?.values &&
         typeof info.values === 'object' &&
-        info.values[value] != undefined
+        info.values[String(value)] != undefined
       ) {
-        value = info.values[value]
+        value = info.values[String(value)]
       }
 
       if (typeof node.properties[property] == 'number') {
-        value = Number(value)
+        value = Number(value)
       }
+      if (typeof node.properties[property] == 'boolean') {
+        value = Boolean(value)
+      }
       if (type == 'array' || type == 'object') {
-        value = JSON.parse(String(value))
+        if (value == null || String(value).trim() === '') value = type === 'array' ? [] : {}
+        else value = JSON.parse(String(value))
       }
       node.properties[property] = value
-->

4709-4718: Add type augmentation for non-standard ctx.start2D() method.

The runtime guard is good, but CanvasRenderingContext2D doesn't define start2D in standard DOM typings. Per coding guidelines, use proper TypeScript types instead of @ts-expect-error. Add a type augmentation in src/lib/litegraph/src/polyfills.ts (or a dedicated type declaration file) to extend CanvasRenderingContext2D.prototype.start2D, following the pattern already used for the roundRect polyfill. This ensures the non-standard method is explicitly typed and intentional.

browser_tests/tests/useSettingSearch.spec.ts (1)

13-41: Silent skip of extension registration if app is undefined.

Using optional chaining (window['app']?.registerExtension) means if app is not initialized, the test settings won't be registered and the beforeEach silently completes. Subsequent tests that depend on these settings could pass incorrectly or fail with confusing errors.

Consider throwing to fail fast, consistent with other guarded patterns in this PR:

Proposed fix
     await comfyPage.page.evaluate(() => {
-      window['app']?.registerExtension({
+      const app = window['app']
+      if (!app) throw new Error('App not initialized')
+      app.registerExtension({
         name: 'TestSettingsExtension',
browser_tests/tests/extensionAPI.spec.ts (1)

89-119: Replace as unknown as SettingParams with an explicit “extension setting” type (avoid unknown casting).
Right now the cast defeats type-safety and makes it easy to accidentally rely on invalid fields.

Proposed fix
 import type { SettingParams } from '../../src/platform/settings/types'
 import { comfyPageFixture as test } from '../fixtures/ComfyPage'
 
+type ExtensionSettingParams = Omit<SettingParams, 'id'> & { id: string }
+
 test.describe('Topbar commands', () => {
@@
     test('Should allow adding settings', async ({ comfyPage }) => {
       await comfyPage.page.evaluate(() => {
         const app = window['app']
         if (!app) throw new Error('App not initialized')
         app.registerExtension({
@@
               onChange: () => {
                 window.changeCount = (window.changeCount ?? 0) + 1
               }
-            } as unknown as SettingParams
+            } satisfies ExtensionSettingParams
           ]
         })
       })
@@
     test('Should allow setting boolean settings', async ({ comfyPage }) => {
       await comfyPage.page.evaluate(() => {
         const app = window['app']
         if (!app) throw new Error('App not initialized')
         app.registerExtension({
@@
               onChange: () => {
                 window.changeCount = (window.changeCount ?? 0) + 1
               }
-            } as unknown as SettingParams
+            } satisfies ExtensionSettingParams
           ]
         })
       })
Based on learnings, avoid `as unknown as ...` and prefer proper typing.

Also applies to: 121-154

browser_tests/fixtures/utils/litegraphUtils.ts (2)

149-176: Remove debug console.log from page.evaluate (noisy + slows CI).

Proposed fix
         const rawPos = node.getConnectionPos(type === 'input', index)
         const convertedPos = app.canvas.ds.convertOffsetToCanvas(rawPos)
-
-        // Debug logging - convert Float64Arrays to regular arrays for visibility
-
-        console.log(
-          `NodeSlotReference debug for ${type} slot ${index} on node ${id}:`,
-          {
-            nodePos: [node.pos[0], node.pos[1]],
-            nodeSize: [node.size[0], node.size[1]],
-            rawConnectionPos: [rawPos[0], rawPos[1]],
-            convertedPos: [convertedPos[0], convertedPos[1]],
-            currentGraphType: graph.constructor.name
-          }
-        )
 
         return convertedPos

223-324: Use app.canvas.graph consistently for accessing the current graph (critical for subgraph support).

getPosition() correctly uses app.canvas.graph, which reflects the active graph whether in the main graph or a subgraph. However, getSocketPosition() and getValue() use app.graph, which always returns the root graph. When a user navigates into a subgraph, these methods will fail to find nodes with "Node not found" errors because they're searching in the root graph instead of the current subgraph.

Replace app.graph with app.canvas.graph in both getSocketPosition() and getValue() to match the correct pattern already established in getPosition().

src/lib/litegraph/src/LGraphNode.ts (2)

925-934: Update widgets_values type to allow null instead of using type assertions.

The serialized widgets_values array uses sparse indexing and intentionally stores null for missing widget slots. The type definition in ISerialisedNode should be Array<TWidgetValue | null> rather than TWidgetValue[], eliminating the need for the null as unknown as TWidgetValue assertion.

Required changes
-// in src/lib/litegraph/src/types/serialisation.ts
-  widgets_values?: TWidgetValue[]
+  widgets_values?: Array<TWidgetValue | null>
-        o.widgets_values[i] = widget
-          ? widget.value
-          : (null as unknown as TWidgetValue)
+        o.widgets_values[i] = widget ? widget.value : null

1889-1916: Remove the redundant toLowerCase() call and unsound type assertion.

Since all TWidgetType values are already lowercase ('toggle', 'number', 'slider', etc.) and Type is constrained to TWidgetType, the toLowerCase() call is redundant and the as Type cast is both unnecessary and violates the coding guideline prohibiting type assertions. Store the type directly without casting:

const w: IBaseWidget & { type: Type } = {
  type: type,
  name: name,
  ...
}

Or simply:

const w: IBaseWidget & { type: Type } = {
  type,
  name,
  ...
}
browser_tests/fixtures/ComfyPage.ts (3)

891-1055: Avoid fixed in-page timeouts for UI emergence (flaky); poll instead.

The setTimeout(100/200ms) inside page.evaluate() is a hidden waitForTimeout equivalent and can be brittle under load/CI. Prefer polling for the condition (menu/dialog) with a bounded timeout inside the evaluate, or remove the sleeps and rely on Playwright-side waitForSelector/toPass only. Based on learnings, avoid waitForTimeout-style sleeps in Playwright tests.


1854-1871: Move focus evaluation inside toPass() callback and fix negation handling in message.

The isFocused value is evaluated once before toPass(), preventing retries from observing focus state changes. Move the evaluation inside the callback so retries can re-evaluate as the DOM state changes.

Also, the message incorrectly uses isFocused to conditionally add "not " instead of respecting this.isNot, causing wrong assertion messages when using .not.toHaveFocus().

Proposed fix
 async toHaveFocus(
   this: MatcherContext,
   locator: Locator,
   options = { timeout: 256 }
 ) {
-  const isFocused = await locator.evaluate(
-    (el) => el === document.activeElement
-  )
-
   await expect(async () => {
-    expect(isFocused).toBe(!this.isNot)
+    const isFocused = await locator.evaluate((el) => el === document.activeElement)
+    expect(isFocused).toBeTruthy()
   }).toPass(options)

+  const isFocused = await locator.evaluate((el) => el === document.activeElement)
   return {
-    pass: isFocused,
-    message: () => `Expected element to ${isFocused ? 'not ' : ''}be focused.`
+    pass: isFocused,
+    message: () =>
+      `Expected element ${this.isNot ? 'not ' : ''}to be focused.`
   }
 }

1823-1848: Custom matcher violates Playwright contract: pass must reflect actual condition, not !this.isNot.

The implementation returns pass: !this.isNot independent of the actual assertion outcome. Per Playwright's custom matcher contract, pass must be the actual boolean result (e.g., whether the node is pinned), and the framework automatically inverts both the pass value and message when .not is used. Manually applying .not to the assertion and hardcoding pass based on negation breaks the contract.

Current behavior: .not.toBePinned() fails only because pass: !true, not because of actual state.

Correct fix: return pass: conditionPass (the actual result from getValue()), remove manual .not application, and let Playwright handle inversion.

Corrected implementation
 const makeMatcher = function <T>(
   getValue: (node: NodeReference) => Promise<T> | T,
   type: string
 ) {
   return async function (
     this: MatcherContext,
     node: NodeReference,
     options?: { timeout?: number; intervals?: number[] }
   ) {
-    const value = await getValue(node)
-    let assertion = expect(
-      value,
-      'Node is ' + (this.isNot ? '' : 'not ') + type
-    )
-    if (this.isNot) {
-      assertion = assertion.not
-    }
-    await expect(async () => {
-      assertion.toBeTruthy()
-    }).toPass({ timeout: 250, ...options })
+    const conditionPass = await getValue(node)
+    await expect(async () => {
+      expect(Boolean(conditionPass)).toBe(true)
+    }).toPass({ timeout: 250, ...options })

     return {
-      pass: !this.isNot,
-      message: () => 'Node is ' + (this.isNot ? 'not ' : '') + type
+      pass: Boolean(conditionPass),
+      message: () => conditionPass
+        ? `Node is not ${type}`
+        : `Node is ${type}`
     }
   }
 }
src/scripts/ui.ts (2)

237-243: Avoid unsafe cast for list type default.

text.toLowerCase() as 'queue' | 'history' will silently produce an invalid type if text changes. Prefer an explicit mapping.

Proposed fix
 constructor(text: string, type?: 'queue' | 'history', reverse?: boolean) {
   this.#text = text
-  this.#type = type || (text.toLowerCase() as 'queue' | 'history')
+  const derived =
+    text.toLowerCase() === 'queue'
+      ? 'queue'
+      : text.toLowerCase() === 'history'
+        ? 'history'
+        : undefined
+  this.#type = type ?? derived ?? 'queue'
   this.#reverse = reverse || false
   this.element = $el('div.comfy-list') as HTMLDivElement
   this.element.style.display = 'none'
 }

249-296: Avoid in-place reverse() on API response arrays.

reverse() mutates the array returned from api.getItems(), which can create hard-to-track ordering issues if the object is reused.

Proposed fix
- ...(this.#reverse
-   ? (items[section as keyof typeof items] as TaskItem[]).reverse()
-   : (items[section as keyof typeof items] as TaskItem[])
- ).map((item: TaskItem) => {
+ ...((sectionItems) => {
+   const list = [...sectionItems]
+   return this.#reverse ? list.reverse() : list
+ })((items[section as keyof typeof items] as TaskItem[]) ?? []).map((item: TaskItem) => {
src/extensions/core/nodeTemplates.ts (1)

123-148: Import flow: handle JSON parse errors + don’t await readAsText (void).

JSON.parse(reader.result as string) can throw and currently bubbles out of the onload handler. Also await reader.readAsText(file) doesn’t do anything (readAsText returns void).

Proposed fix
 reader.onload = async () => {
-  const importFile = JSON.parse(reader.result as string)
-  if (importFile?.templates) {
-    for (const template of importFile.templates) {
-      if (template?.name && template?.data) {
-        this.templates.push(template)
-      }
-    }
-    await this.store()
-  }
+  try {
+    const importFile = JSON.parse(String(reader.result ?? 'null'))
+    if (importFile?.templates) {
+      for (const template of importFile.templates) {
+        if (template?.name && template?.data) {
+          this.templates.push(template)
+        }
+      }
+      await this.store()
+    }
+  } catch (error) {
+    useToastStore().addAlert(error instanceof Error ? error.message : String(error))
+  }
 }
-reader.readAsText(file)
+reader.readAsText(file)
src/lib/litegraph/src/LGraph.ts (2)

1227-1246: Avoid type assertions here; prefer runtime method checks (litegraph guideline).

Instead of casting node as LGraphNode & { onTrigger?: ... }, do 'onTrigger' in node + typeof === 'function'. This keeps “assertions last resort” intact. Based on learnings, type assertions should be a last resort in litegraph.


2203-2217: Verify meaning of this.version: schema version vs library version.

_configureBase writes data.version into this.version, but serialize() later emits version: LiteGraph.VERSION (different meaning). If both meanings must exist, consider separating names (e.g., schemaVersion vs litegraphVersion) to prevent future misuse.

Comment on lines 28 to 35
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
workflow?: { activeWorkflow?: { filename?: string } }
}
return extMgr.workflow?.activeWorkflow?.filename
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting repeated type to reduce duplication.

The inline type shape for extensionManager is repeated three times. While acceptable in test code, extracting to a local type alias would improve maintainability.

♻️ Optional: Extract shared type
// At top of file or in test describe block
type ExtMgrShape = {
  workflow?: {
    activeWorkflow?: {
      filename?: string
      delete?: () => Promise<void>
    }
  }
}

// Then in evaluate callbacks:
const extMgr = app.extensionManager as ExtMgrShape

Also applies to: 47-54

🤖 Prompt for AI Agents
In @browser_tests/tests/browserTabTitle.spec.ts around lines 28 - 35, The inline
type for app.extensionManager is duplicated in multiple evaluate callbacks
(e.g., the callback used to compute workflowName and the one at lines 47-54);
extract a shared local type alias (for example ExtMgrShape) scoped at the top of
the test file or inside the describe block, include the optional
workflow.activeWorkflow.filename and delete signature as needed, and then cast
extensionManager with that alias (e.g., app.extensionManager as ExtMgrShape) in
each comfyPage.page.evaluate callback to remove duplication and improve
maintainability.

Comment on lines 19 to 23
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['4']
const graph = window['graph'] as GraphWithNodes
const node = graph._nodes_by_id['4']
node.widgets.push(node.widgets[0])
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a guard for consistency with other test files

Other test files in this PR add explicit guards (e.g., if (!graph) throw new Error('Graph not initialized')). While the test will fail if the graph doesn't exist, an explicit guard provides clearer error messages.

Optional improvement
    await comfyPage.page.evaluate(() => {
      const graph = window['graph'] as GraphWithNodes
+      if (!graph) throw new Error('Graph not initialized')
      const node = graph._nodes_by_id['4']
      node.widgets.push(node.widgets[0])
    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['4']
const graph = window['graph'] as GraphWithNodes
const node = graph._nodes_by_id['4']
node.widgets.push(node.widgets[0])
})
await comfyPage.page.evaluate(() => {
const graph = window['graph'] as GraphWithNodes
if (!graph) throw new Error('Graph not initialized')
const node = graph._nodes_by_id['4']
node.widgets.push(node.widgets[0])
})
🤖 Prompt for AI Agents
In @browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts around lines
19 - 23, Add an explicit guard before manipulating the graph inside the
comfyPage.page.evaluate callback: ensure window['graph'] exists and is typed
(GraphWithNodes) and throw a clear Error like "Graph not initialized" if not;
then proceed to access graph._nodes_by_id['4'] and node.widgets, so replace the
silent assumption in the comfyPage.page.evaluate block with a check that throws
a descriptive error when graph or the target node is missing.

Comment on lines 110 to 117
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
const app = window['app']
if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return
const widget = app.graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
window.widgetValue = value
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Early return may silently mask test failures.

The guard if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return silently exits without setting up the callback. If the app/graph/widget is missing, the test will pass even though the actual behavior wasn't tested. Consider throwing an error instead for fail-fast behavior, consistent with other tests in this PR.

🐛 Suggested fix
       await comfyPage.page.evaluate(() => {
         const app = window['app']
-        if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return
-        const widget = app.graph.nodes[0].widgets[0]
+        if (!app?.graph?.nodes?.[0]?.widgets?.[0]) {
+          throw new Error('App, graph, or widget not found')
+        }
+        const widget = app.graph.nodes[0].widgets[0]!
         widget.callback = (value: number) => {
           window.widgetValue = value
         }
       })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
const app = window['app']
if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return
const widget = app.graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
window.widgetValue = value
}
})
await comfyPage.page.evaluate(() => {
const app = window['app']
if (!app?.graph?.nodes?.[0]?.widgets?.[0]) {
throw new Error('App, graph, or widget not found')
}
const widget = app.graph.nodes[0].widgets[0]!
widget.callback = (value: number) => {
window.widgetValue = value
}
})
🤖 Prompt for AI Agents
In @browser_tests/tests/widget.spec.ts around lines 110 - 117, Replace the
silent early return in the comfyPage.page.evaluate callback with a fail-fast
error: inside the evaluate block that accesses window['app'] and checks
app?.graph?.nodes?.[0]?.widgets?.[0], throw a descriptive Error when the widget
is missing instead of returning, then proceed to assign widget.callback =
(value: number) => { window.widgetValue = value }; this ensures tests fail
loudly if the app/graph/widget is not present.

Comment on lines 133 to 140
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
const app = window['app']
if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return
const widget = app.graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
window.widgetValue = value
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same early return concern as above.

This guard also uses early return which could silently skip the callback setup without failing the test.

🐛 Suggested fix
       await comfyPage.page.evaluate(() => {
         const app = window['app']
-        if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return
-        const widget = app.graph.nodes[0].widgets[0]
+        if (!app?.graph?.nodes?.[0]?.widgets?.[0]) {
+          throw new Error('App, graph, or widget not found')
+        }
+        const widget = app.graph.nodes[0].widgets[0]!
         widget.callback = (value: number) => {
           window.widgetValue = value
         }
       })
🤖 Prompt for AI Agents
In @browser_tests/tests/widget.spec.ts around lines 133 - 140, The evaluate
callback currently silently returns when the widget is missing
(window['app']?.graph?.nodes?.[0]?.widgets?.[0]) which can hide failures;
instead, inside comfyPage.page.evaluate ensure the widget exists and throw an
error if it does not (e.g., check app/graph/nodes[0]/widgets[0] and throw new
Error('widget not found') when absent) before assigning widget.callback so the
test fails loudly; update the block referencing window['app'],
app.graph.nodes[0].widgets[0], and widget.callback accordingly.

Comment on lines +130 to +137
## Never Use `@ts-ignore` or `@ts-expect-error`

These directives suppress all errors on a line, making it easy to accidentally mask serious bugs. Instead:

1. Fix the underlying type issue
2. Use a type guard to narrow the type
3. If truly unavoidable, use a targeted cast with explanation

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify the relationship between "never use @ts-expect-error" and "targeted cast with explanation".

The guidance on lines 134-136 suggests a "targeted cast with explanation" as a fallback option, but the earlier section (lines 10-12) states "Never Use Type Assertions (as)". Consider clarifying when a targeted cast might be acceptable (e.g., in type guard implementations, test mocks) to avoid confusion.

📝 Suggested clarification
 1. Fix the underlying type issue
 2. Use a type guard to narrow the type
-3. If truly unavoidable, use a targeted cast with explanation
+3. If truly unavoidable (e.g., type guard implementations, test mocks with `Partial<T>`), use a targeted cast with explanation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Never Use `@ts-ignore` or `@ts-expect-error`
These directives suppress all errors on a line, making it easy to accidentally mask serious bugs. Instead:
1. Fix the underlying type issue
2. Use a type guard to narrow the type
3. If truly unavoidable, use a targeted cast with explanation
## Never Use `@ts-ignore` or `@ts-expect-error`
These directives suppress all errors on a line, making it easy to accidentally mask serious bugs. Instead:
1. Fix the underlying type issue
2. Use a type guard to narrow the type
3. If truly unavoidable (e.g., type guard implementations, test mocks with `Partial<T>`), use a targeted cast with explanation
🤖 Prompt for AI Agents
In @docs/typescript/type-safety.md around lines 130 - 137, Clarify the apparent
contradiction between the "Never Use Type Assertions (`as`)" guidance and the
fallback "targeted cast with explanation" in the "Never Use `@ts-ignore` or
`@ts-expect-error`" section by explicitly stating when a targeted cast is
acceptable: mention it as a last-resort, well-documented exception used only for
narrow cases such as type guard implementations, test mocks, interop with
third-party libs where types are known but not expressed, or migrating legacy
code; update the text around the headings "Never Use Type Assertions (`as`)" and
"Never Use `@ts-ignore` or `@ts-expect-error`" to include this reconciliation
and require adding a short comment explaining why the cast is safe and linking
to an example pattern or test demonstrating the invariant.

Comment on lines +119 to 122
if (!over || !currentImgs) return
// Set the overIndex so Open Image etc work
// @ts-expect-error fixme ts strict error
const idx = currentImgs.indexOf(over)
const idx = currentImgs.indexOf(over as HTMLImageElement)
node.overIndex = idx
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using type guard instead of type assertion.

The cast over as HTMLImageElement is a type assertion which the coding guidelines discourage. Since indexOf returns -1 for non-members anyway, this works, but a type guard would be more explicit.

♻️ Optional improvement using type guard
       if (!over || !currentImgs) return
       // Set the overIndex so Open Image etc work
-      const idx = currentImgs.indexOf(over as HTMLImageElement)
+      const idx = over instanceof HTMLImageElement ? currentImgs.indexOf(over) : -1
       node.overIndex = idx
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!over || !currentImgs) return
// Set the overIndex so Open Image etc work
// @ts-expect-error fixme ts strict error
const idx = currentImgs.indexOf(over)
const idx = currentImgs.indexOf(over as HTMLImageElement)
node.overIndex = idx
if (!over || !currentImgs) return
// Set the overIndex so Open Image etc work
const idx = over instanceof HTMLImageElement ? currentImgs.indexOf(over) : -1
node.overIndex = idx
🤖 Prompt for AI Agents
In @src/scripts/ui/imagePreview.ts around lines 119 - 122, The code uses a type
assertion over as HTMLImageElement when computing the index; replace this with a
type guard by checking if over is an instance of HTMLImageElement before calling
currentImgs.indexOf. Concretely, after the existing null checks on over and
currentImgs, use "if (over instanceof HTMLImageElement) { const idx =
currentImgs.indexOf(over); node.overIndex = idx; } else { node.overIndex = -1;
}" so you avoid the cast and explicitly handle non-image values while keeping
node.overIndex deterministic.

Comment on lines +355 to 361
export const adjustColor: <T extends string | null | undefined>(
color: T,
options: ColorAdjustOptions
) => string = memoize(
) => T | string = memoize(
applyColorAdjustments,
(color: string, options: ColorAdjustOptions): string =>
(color: string | null | undefined, options: ColorAdjustOptions): string =>
`${color}-${JSON.stringify(options)}`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cache key collision between null/undefined values and literal strings.

The template literal ${color} produces identical strings for null and the string "null" (similarly for undefined). This causes cache collisions where adjustColor(null, opts) and adjustColor("null", opts) share the same key but return different results.

🔧 Suggested fix using JSON.stringify
 export const adjustColor: <T extends string | null | undefined>(
   color: T,
   options: ColorAdjustOptions
 ) => T | string = memoize(
   applyColorAdjustments,
   (color: string | null | undefined, options: ColorAdjustOptions): string =>
-    `${color}-${JSON.stringify(options)}`
+    `${JSON.stringify(color)}-${JSON.stringify(options)}`
 )

This produces distinct keys: "null" for null vs "\"null\"" for the string "null".

🤖 Prompt for AI Agents
In @src/utils/colorUtil.ts around lines 355 - 361, The memoized adjustColor
implementation builds a cache key using `${color}-${JSON.stringify(options)}`,
which conflates null/undefined with the literal strings "null"/"undefined";
change the keyResolver passed to memoize so it JSON.stringify() the color as
well (e.g., JSON.stringify(color) + '-' + JSON.stringify(options)) to produce
distinct keys for null/undefined versus the string literals; update the memoize
call that wraps applyColorAdjustments in the adjustColor declaration to use this
new key format.

// @ts-expect-error fixme ts strict error
graph.beforeChange()

const deserialised = JSON.parse(data)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding runtime validation for parsed data.

JSON.parse returns any, so deserialised lacks type safety. Since this is a deprecated function parsing legacy data, this is acceptable. However, for consistency with the PR's type safety goals, consider adding minimal runtime guards:

🔧 Optional: Add defensive guards for parsed structure
     const deserialised = JSON.parse(data)
+    if (!deserialised?.nodes || !Array.isArray(deserialised.nodes)) return
+    if (!deserialised?.links || !Array.isArray(deserialised.links)) return

This provides runtime safety without requiring a type assertion, which aligns with the learnings that type assertions should be avoided.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const deserialised = JSON.parse(data)
const deserialised = JSON.parse(data)
if (!deserialised?.nodes || !Array.isArray(deserialised.nodes)) return
if (!deserialised?.links || !Array.isArray(deserialised.links)) return
🤖 Prompt for AI Agents
In @src/utils/vintageClipboard.ts at line 95, Add minimal runtime validation
after the JSON.parse call that sets `deserialised` in vintageClipboard.ts: check
that `deserialised` is an object and that expected legacy fields (e.g., specific
keys or types you rely on later) exist and have the correct primitive types
before using them; if validation fails, handle it safely (return early, throw a
clear error, or fallback) so you avoid untyped assumptions instead of using a
type assertion on `deserialised`.

Comment on lines 69 to 74
const nodeNames = computed(() => {
// @ts-expect-error comfy_nodes is an Algolia-specific field
const { comfy_nodes } = nodePack
return comfy_nodes ?? []
const packWithAlgolia = nodePack as typeof nodePack & {
comfy_nodes?: string[]
}
return packWithAlgolia.comfy_nodes ?? []
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good improvement replacing @ts-expect-error with explicit type assertion.

The inline type extension pattern is acceptable here since components['schemas']['Node'] is auto-generated from the registry API schema and comfy_nodes appears to be an Algolia-specific property not present in the base type.

If this pattern is needed elsewhere, consider extracting a reusable type alias:

type NodeWithAlgoliaFields = components['schemas']['Node'] & {
  comfy_nodes?: string[]
}

This would centralize the Algolia augmentation and make it easier to maintain if additional Algolia-specific fields need to be added.

🤖 Prompt for AI Agents
In @src/workbench/extensions/manager/components/manager/infoPanel/InfoTabs.vue
around lines 69 - 74, Replace the inline ad-hoc type assertion in the nodeNames
computed getter with a reusable type alias: define a NodeWithAlgoliaFields type
as components['schemas']['Node'] & { comfy_nodes?: string[] } and cast nodePack
to that type inside the nodeNames computed (referencing nodeNames and nodePack)
so the Algolia-specific comfy_nodes augmentation is centralized and easier to
reuse/maintain.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +84 to 94
const app = window.app
if (!app) {
resolve(undefined)
return
}
app.ui.settings.addEventListener('Comfy.ColorPalette.change', resolve, {
once: true
})

setTimeout(resolve, 5000)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Ensure the settings listener is removed if the timeout wins (leaks across tests).

setTimeout(resolve, 5000) can resolve without removing the Comfy.ColorPalette.change listener (since only { once: true } removes it on event). Consider cleaning up on timeout.

🤖 Prompt for AI Agents
In @browser_tests/fixtures/ComfyPage.ts around lines 84 - 94, The timeout path
can resolve without removing the Comfy.ColorPalette.change listener, leaking
across tests; fix by capturing the event handler in a const (e.g., handler =
(ev) => resolve(ev)), add it via
app.ui.settings.addEventListener('Comfy.ColorPalette.change', handler, { once:
true }), and in the timeout callback call
app.ui.settings.removeEventListener('Comfy.ColorPalette.change', handler) (and
clear the timeout if needed) before calling resolve so the listener is always
cleaned up whether the event or timeout wins.

Comment on lines 143 to 154
await this.page.waitForFunction(
() => window['app']?.extensionManager?.workflow?.isBusy === false,
() => {
const app = window.app
if (!app) return true
const extMgr = app.extensionManager as {
workflow?: { isBusy?: boolean }
}
return extMgr.workflow?.isBusy === false
},
undefined,
{ timeout: 3000 }
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Busy-wait condition can hang when workflow extension is absent/undefined.

extMgr.workflow?.isBusy === false is false when workflow or isBusy is undefined, causing a guaranteed 3s timeout even when there’s nothing to wait for. Prefer “not busy” semantics.

Proposed fix
 await this.page.waitForFunction(
   () => {
     const app = window.app
     if (!app) return true
     const extMgr = app.extensionManager as {
       workflow?: { isBusy?: boolean }
     }
-    return extMgr.workflow?.isBusy === false
+    // Treat missing workflow/isBusy as "not busy" for test stability.
+    return extMgr.workflow?.isBusy !== true
   },
   undefined,
   { timeout: 3000 }
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.page.waitForFunction(
() => window['app']?.extensionManager?.workflow?.isBusy === false,
() => {
const app = window.app
if (!app) return true
const extMgr = app.extensionManager as {
workflow?: { isBusy?: boolean }
}
return extMgr.workflow?.isBusy === false
},
undefined,
{ timeout: 3000 }
)
await this.page.waitForFunction(
() => {
const app = window.app
if (!app) return true
const extMgr = app.extensionManager as {
workflow?: { isBusy?: boolean }
}
// Treat missing workflow/isBusy as "not busy" for test stability.
return extMgr.workflow?.isBusy !== true
},
undefined,
{ timeout: 3000 }
)
🤖 Prompt for AI Agents
In @browser_tests/fixtures/ComfyPage.ts around lines 143 - 154, The
waitForFunction busy-check treats undefined workflow/isBusy as false which
causes unnecessary timeout; update the predicate inside
this.page.waitForFunction so it treats missing workflow or missing isBusy as
"not busy" (e.g., use extMgr.workflow?.isBusy !== true or explicitly check
extMgr.workflow == null || extMgr.workflow.isBusy === false) so the function
returns true when the workflow extension is absent; keep the surrounding
structure (this.page.waitForFunction, extMgr, workflow, isBusy) intact and only
change the boolean condition to "not true" semantics.

Comment on lines +10 to +16
type TestPalette = Omit<Palette, 'colors'> & {
colors: {
node_slot: Record<string, string>
litegraph_base: Partial<Palette['colors']['litegraph_base']>
comfy_base: Partial<Palette['colors']['comfy_base']>
}
}
Copy link
Contributor

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
# Find where custom palettes are validated/parsed and whether partial palette objects are accepted.
rg -n "CustomColorPalettes|addCustomColorPalette|paletteSchema|ColorPalette" src | head -n 200

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 16777


🏁 Script executed:

cat -n src/schemas/colorPaletteSchema.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4678


🏁 Script executed:

head -n 25 browser_tests/tests/colorPalette.spec.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 771


Remove redundant Partial<> from litegraph_base and comfy_base or align node_slot with the schema.

The source Palette type already accepts partial colors via partialColorsSchema, so the Partial<> wrappers on litegraph_base and comfy_base are redundant. However, TestPalette uses Record<string, string> for node_slot, which is looser than the schema's nodeSlotSchema.partial() and allows arbitrary keys. Either use the actual partial schema type for consistency or document why the test needs this flexibility.

🤖 Prompt for AI Agents
In @browser_tests/tests/colorPalette.spec.ts around lines 10 - 16, The
TestPalette definition is inconsistent with the source Palette: remove the
redundant Partial<> wrappers from litegraph_base and comfy_base (they already
accept partial colors via partialColorsSchema) and replace the overly-permissive
node_slot: Record<string,string> with the schema-aligned type (e.g. use
Partial<Palette['colors']['node_slot']> or the exact type derived from
nodeSlotSchema.partial()) so the test type matches the real Palette shape or add
a short comment explaining why arbitrary keys are required.

Comment on lines 185 to 193
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
}
extMgr.colorPalette?.addCustomColorPalette?.(p)
}, customColorPalettes.obsidian_dark)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast if addCustomColorPalette is missing (don’t optional-chain a required test API).
The test intention is “can add”; silently skipping the call makes failures harder to root-cause.

Proposed fix
     await comfyPage.page.evaluate((p) => {
       const app = window['app']
       if (!app) throw new Error('App not initialized')
       const extMgr = app.extensionManager as {
         colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
       }
-      extMgr.colorPalette?.addCustomColorPalette?.(p)
+      if (!extMgr.colorPalette?.addCustomColorPalette) {
+        throw new Error('colorPalette.addCustomColorPalette not available')
+      }
+      extMgr.colorPalette.addCustomColorPalette(p)
     }, customColorPalettes.obsidian_dark)
🤖 Prompt for AI Agents
In @browser_tests/tests/colorPalette.spec.ts around lines 185 - 193, The test
currently optional-chains extMgr.colorPalette?.addCustomColorPalette?.(p) which
silently skips the call if the API is missing; change the inline evaluate body
to fail fast by asserting the API exists and then calling it directly: retrieve
window['app'], assert app and app.extensionManager exist, assert
extensionManager.colorPalette and its addCustomColorPalette function are defined
(throw a helpful Error if not), then invoke addCustomColorPalette(p) so the test
fails immediately when the required API is absent.

Comment on lines 348 to 355
autoQueueMode!: string
graphHasChanged!: boolean
autoQueueEnabled!: boolean
menuContainer!: HTMLDivElement
queueSize!: Element
restoreMenuPosition!: () => void
loadFile!: () => void

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Initialize ComfyUI flags to concrete defaults (avoid “undefined means false” drift).

The definite assignment fields are now type-safe, but runtime values are still undefined until user interaction. Consider setting defaults (e.g., autoQueueEnabled = false, graphHasChanged = false, autoQueueMode = 'instant') to make behavior explicit.

Also applies to: 356-372, 406-409

Comment on lines +666 to 668
// Initialize with placeholder text before first status update
this.queueSize.textContent = 'Queue size: X'
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

New user-visible string should be localized.

'Queue size: X' is user-facing; consider moving it to i18n. As per coding guidelines, use vue-i18n for user-facing strings.

🤖 Prompt for AI Agents
In @src/scripts/ui.ts around lines 666 - 668, The hardcoded user-facing
placeholder "Queue size: X" set at the this.queueSize.textContent assignment
should be moved to i18n; replace the literal with a vue-i18n translation lookup
(e.g., use this.$t or i18n.t) referencing a new key like
"queue.queueSizePlaceholder" and add that key to your locale files with the
value "Queue size: X" (and translations). Ensure the code uses the translation
call where this.queueSize.textContent is set (in src/scripts/ui.ts) so the UI
string is localized.

Comment on lines 29 to 40
popup: ComfyPopup | null = null
element: HTMLElement
overIcon: string
iconSize: number
content: string | HTMLElement
icon: string
tooltip: string
classList: ClassList
hidden: boolean
enabled: boolean
action: (e: Event, btn: ComfyButton) => void
overIcon!: string
iconSize!: number
content!: string | HTMLElement
icon!: string
tooltip!: string
classList!: ClassList
hidden!: boolean
enabled!: boolean
action!: (e: Event, btn: ComfyButton) => void

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t “type away” optional props with !—make fields truly optional or provide defaults.

prop(..., icon) / prop(..., action) can return undefined at runtime, but fields are declared as required (e.g., action!: (...) => void). This is brittle and defeats the goal of “type safety without suppressions”. Based on learnings, avoid assertion crutches and model the real optionality.

Proposed fix (one direction: default + required field)
 type ComfyButtonProps = {
   ...
-  action?: (e: Event, btn: ComfyButton) => void
+  action?: (e: Event, btn: ComfyButton) => void
   ...
 }

 export class ComfyButton implements ComfyComponent<HTMLElement> {
   ...
-  action!: (e: Event, btn: ComfyButton) => void
+  action!: (e: Event, btn: ComfyButton) => void
   ...
   constructor({ ..., action, ... }: ComfyButtonProps) {
     ...
-    this.action = prop(this, 'action', action)!
+    this.action = prop(this, 'action', action ?? (() => {}))!
     this.element.addEventListener('click', (e) => {
       ...
-      this.action?.(e, this)
+      this.action(e, this)
     })

Also applies to: 72-115, 123-124

Comment on lines +164 to 171
for (const el of [this.element, popup.element]) {
el.addEventListener('mouseenter', () => {
this.popup.open = !!++this.#over
popup.open = !!++this.#over
})
el.addEventListener('mouseleave', () => {
this.popup.open = !!--this.#over
popup.open = !!--this.#over
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against #over counter underflow (defensive).

popup.open = !!--this.#over can go negative if events get out of balance (e.g., nested elements / missed mouseleave). Consider clamping at 0.

🤖 Prompt for AI Agents
In @src/scripts/ui/components/button.ts around lines 164 - 171, The hover
counter can underflow; change the mouseenter/mouseleave handlers to update the
private counter without going below 0 and derive popup.open from whether the
counter is > 0. Specifically, replace the ++/-- logic around this.#over in the
event listeners attached to this.element and popup.element with safe increments
and safe decrements (clamp to zero, e.g., this.#over = Math.max(0, this.#over -
1)) and set popup.open = this.#over > 0 after updating so the counter never
becomes negative and open reflects the clamped count.

* ISerialisedGraph is the LiteGraph serialization format.
*
* TODO: Align these schemas to eliminate the need for this cast.
* @see https://github.com/Comfy-Org/ComfyUI_frontend/issues/XXXX
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update placeholder issue number.

The @see reference contains a placeholder XXXX instead of an actual issue number. Either create the tracking issue and update this reference, or remove the line if no issue exists yet.

-* @see https://github.com/Comfy-Org/ComfyUI_frontend/issues/XXXX
+* @see https://github.com/Comfy-Org/ComfyUI_frontend/issues/[ACTUAL_NUMBER]
🤖 Prompt for AI Agents
In @src/types/workflowSchemaTypes.ts at line 12, The @see JSDoc line in
workflowSchemaTypes.ts currently uses a placeholder
"https://github.com/Comfy-Org/ComfyUI_frontend/issues/XXXX"; replace "XXXX" with
the actual issue number for the tracking issue (or, if no issue will be created,
remove the entire @see line) so the reference points to a valid GitHub issue;
search for the @see line in the file to locate and update it.

@DrJKL DrJKL requested a review from Myestery January 12, 2026 22:10
@DrJKL
Copy link
Contributor Author

DrJKL commented Jan 12, 2026

@coderabbitai review full

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

@DrJKL: I'll conduct a comprehensive full review of this PR. Given the extensive scope of this type safety refactoring, I'll examine:

Key Review Areas:

  • ✅ Type guard implementations replacing @ts-expect-error suppressions
  • ✅ New type definitions and interfaces (SubgraphGraph, GroupNodeTypes, WorkflowAsGraph, etc.)
  • ✅ Browser test type improvements and global type declarations
  • ✅ LiteGraph type system enhancements (ILinkRouting, window augmentations)
  • ✅ Widget and DOM element type safety patterns
  • ✅ The remaining PrimeVue type issue suppressions
  • ✅ Overall architectural improvements to type safety

Focus Points:

  • Correctness of type guards and narrowing patterns
  • Consistency of null-safety approaches
  • Proper handling of optional chaining vs. assertions
  • Test mock implementations and type safety
  • Breaking changes or API surface modifications

Starting the full review now across all 180 changed files.


🧠 Learnings used
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} : Implement proper TypeScript types throughout the codebase

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:24.164Z
Learning: The project has CI automation that handles code formatting (pnpm format) automatically, so manual formatting suggestions should not be provided in code reviews for the Comfy-Org/ComfyUI_frontend repository.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T00:24:17.695Z
Learning: Applies to src/**/*.{ts,tsx,vue} : Watch out for Code Smells and refactor to avoid them

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} : Avoid using ts-expect-error; use proper TypeScript types instead
✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 47

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (16)
src/scripts/metadata/png.ts (1)

45-55: Promise may never settle if the guard condition is false.

The type guard correctly replaces the @ts-expect-error, but the promise now silently hangs if event.target?.result is not an ArrayBuffer. While readAsArrayBuffer should produce an ArrayBuffer on success, defensive code should ensure the promise always settles.

Additionally, there's no onerror handler for the FileReader, so read failures will also leave the promise hanging.

🔧 Proposed fix to ensure the promise always settles
 export function getFromPngFile(file: File) {
   return new Promise<Record<string, string>>((r) => {
     const reader = new FileReader()
     reader.onload = (event) => {
       if (event.target?.result instanceof ArrayBuffer) {
         r(getFromPngBuffer(event.target.result))
+      } else {
+        r({})
       }
     }
+    reader.onerror = () => r({})
 
     reader.readAsArrayBuffer(file)
   })
 }

Alternatively, reject on error for more explicit failure handling:

🔧 Alternative: reject on failure
-export function getFromPngFile(file: File) {
-  return new Promise<Record<string, string>>((r) => {
+export function getFromPngFile(file: File) {
+  return new Promise<Record<string, string>>((resolve, reject) => {
     const reader = new FileReader()
     reader.onload = (event) => {
       if (event.target?.result instanceof ArrayBuffer) {
-        r(getFromPngBuffer(event.target.result))
+        resolve(getFromPngBuffer(event.target.result))
+      } else {
+        reject(new Error('Failed to read file as ArrayBuffer'))
       }
     }
+    reader.onerror = () => reject(reader.error ?? new Error('FileReader error'))
 
     reader.readAsArrayBuffer(file)
   })
 }
src/utils/nodeDefUtil.test.ts (1)

219-226: Consider using assertion helper for consistency.

This test block asserts result is not null but then uses optional chaining (?.) on subsequent lines. For consistency with other tests, consider using the assertion pattern or removing the optional chaining since the null check already passed.

♻️ Suggested refactor for consistency
         const result = mergeInputSpec(spec1, spec2)
 
         expect(result).not.toBeNull()
-        expect(result?.[0]).toBe('STRING')
-        expect(result?.[1]?.default).toBe('value2')
-        expect(result?.[1]?.tooltip).toBe('Tooltip 2')
-        expect(result?.[1]?.step).toBe(1)
+        expect(result![0]).toBe('STRING')
+        expect(result![1]?.default).toBe('value2')
+        expect(result![1]?.tooltip).toBe('Tooltip 2')
+        expect(result![1]?.step).toBe(1)

Or create an assertion helper for STRING specs similar to the numeric/combo patterns.

src/scripts/ui/imagePreview.ts (1)

22-25: Potential runtime error if imgs is empty.

Line 23-24 accesses imgs[0].naturalWidth and imgs[0].naturalHeight without checking if the array is non-empty. If calculateImageGrid is called with an empty array, this will throw a TypeError.

Consider adding an early return or guard:

🛡️ Proposed defensive guard
 export function calculateImageGrid(
   imgs: ImageLike[],
   dw: number,
   dh: number
 ): {
   cellWidth: number
   cellHeight: number
   cols: number
   rows: number
   shiftX: number
 } {
+  if (imgs.length === 0) {
+    return { cellWidth: 0, cellHeight: 0, cols: 0, rows: 0, shiftX: 0 }
+  }
+
   let best = 0
   let w = imgs[0].naturalWidth
src/components/dialog/content/MissingCoreNodesMessage.test.ts (1)

17-31: Consider simplifying the mock node creation.

The Object.assign(Object.create(null), {...}) pattern creates a null-prototype object, which is unusual for mocks. Unless this is intentionally matching LiteGraph's internal object structure, a simpler approach would suffice:

function createMockNode(type: string, version?: string): LGraphNode {
  return {
    type,
    properties: { cnr_id: 'comfy-core', ver: version },
    // ... rest of properties
  } as Partial<LGraphNode> as LGraphNode
}

This would also align with the learned pattern for partial mock implementations. If the null-prototype behavior is intentional for test isolation, please add a brief comment explaining why.

src/components/common/TreeExplorerTreeNode.test.ts (1)

23-31: Use explicit Partial pattern for mock object.

The mock object appears to partially implement the RenderedTreeExplorerNode interface. Per repository conventions, use the as Partial<T> as T pattern to make the incomplete implementation explicit.

♻️ Apply Partial pattern
 const mockNode = {
   key: '1',
   label: 'Test Node',
   leaf: false,
   totalLeaves: 3,
   icon: 'pi pi-folder',
   type: 'folder',
   handleRename: () => {}
-} as RenderedTreeExplorerNode
+} as Partial<RenderedTreeExplorerNode> as RenderedTreeExplorerNode

This explicitly acknowledges the partial implementation while maintaining type safety. Based on learnings.

src/scripts/metadata/avif.ts (2)

278-278: Type mismatch and any usage violate coding guidelines.

Line 278 uses Record<string, any> which violates the project guideline "Never use any type". Additionally, parseExifData returns Record<number, string | undefined> but is assigned to Record<string, any>, creating a type mismatch.

Suggested fix
-    const data: Record<string, any> = parseExifData(exifData)
+    const data = parseExifData(exifData)

Then update the loop to handle the correctly inferred type. Since JavaScript coerces numeric keys to strings in for...in loops, the runtime behavior is preserved, but you may want to explicitly convert the key if needed for downstream string comparisons.


328-345: Implicit undefined return when length is neither 2 nor 4.

The function returns number | undefined but only has explicit returns for length === 2 and length === 4. For any other length, it falls through and implicitly returns undefined. Consider adding an explicit return or throwing an error for unsupported lengths to make the intent clear.

Option: explicit return for clarity
     } else if (length === 4) {
       return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
         0,
         isLittleEndian
       )
     }
+    return undefined
   }
scripts/collect-i18n-node-defs.ts (2)

55-69: getComfyWindow() is not available in the browser context—this will fail at runtime.

page.evaluate() serializes and runs the callback in the browser's JavaScript context. Functions defined in the Node.js script scope (like getComfyWindow, isComfyWindow) are not accessible inside the evaluate callback. This code will throw ReferenceError: getComfyWindow is not defined when executed.

🐛 Proposed fix: inline the guard logic within the evaluate callback
  const nodeDefs: ComfyNodeDefImpl[] = await comfyPage.page.evaluate(
    async () => {
-      const comfyWindow = getComfyWindow()
-      const api = comfyWindow.app.api
+      const win = window as Window & { app?: ComfyApp; LiteGraph?: LiteGraphGlobal }
+      if (!win.app || !win.LiteGraph) {
+        throw new Error('ComfyApp or LiteGraph not found on window')
+      }
+      const api = win.app.api
      const rawNodeDefs = await api.getNodeDefs()
      const { ComfyNodeDefImpl } = await import('../src/stores/nodeDefStore')

      return (
        Object.values(rawNodeDefs)
          // Ignore DevTools nodes (used for internal testing)
          .filter((def: ComfyNodeDef) => !def.name.startsWith('DevTools'))
          .map((def: ComfyNodeDef) => new ComfyNodeDefImpl(def))
      )
    }
  )

Alternatively, you can use Playwright's exposeFunction to make helpers available in the browser context, or simply inline the type assertion with a null check as shown above.


104-119: Same issue: getComfyWindow() is not available in this browser context.

This page.evaluate() callback also references getComfyWindow(), which is defined in Node.js scope and unavailable in the browser.

🐛 Proposed fix: inline the guard and type assertion
        const widgetsMappings = await comfyPage.page.evaluate(
          (args) => {
            const [nodeName, displayName, inputNames] = args
-            const comfyWindow = getComfyWindow()
-            const node = comfyWindow.LiteGraph.createNode(nodeName, displayName)
+            const win = window as Window & { LiteGraph?: LiteGraphGlobal }
+            if (!win.LiteGraph) {
+              throw new Error('LiteGraph not found on window')
+            }
+            const node = win.LiteGraph.createNode(nodeName, displayName)
            if (!node?.widgets?.length) return {}
            return Object.fromEntries(
              node.widgets
                .filter(
                  (w: WidgetInfo) => w?.name && !inputNames.includes(w.name)
                )
                .map((w: WidgetInfo) => [w.name, w.label])
            )
          },
          [nodeDef.name, nodeDef.display_name, inputNames]
        )
src/composables/maskeditor/useCanvasManager.test.ts (1)

127-146: Minor inconsistency: imgCanvas and rgbCanvas include style but implementation doesn't use it.

Looking at useCanvasManager.ts, only maskCanvas.style and canvasBackground.style are accessed. The style: {} on imgCanvas and rgbCanvas is defensive but unnecessary. Consider removing for clarity, or keep if anticipating future use.

src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts (1)

406-423: Duplicate createMockNode helper.

This helper is nearly identical to the one defined at lines 324-337. Consider extracting it to a shared helper at the top of the file alongside createMockNodeDefStore to reduce duplication.

🔧 Suggested refactor

Move the helper to the top of the file (after line 28):

+function createMockNode(
+  type: string,
+  packId?: string,
+  version?: string
+): LGraphNode {
+  return {
+    type,
+    properties: { cnr_id: packId, ver: version },
+    id: 1,
+    title: type,
+    pos: [0, 0],
+    size: [100, 100],
+    flags: {},
+    graph: null,
+    mode: 0,
+    inputs: [],
+    outputs: []
+  } as unknown as LGraphNode
+}

Then remove both nested definitions.

src/scripts/ui/dialog.ts (1)

37-44: Sanitize string HTML input with DOMPurify to prevent XSS attacks.

The innerHTML assignment on line 39 accepts untrusted string input directly without sanitization. Per coding guidelines, all HTML handling in src/**/*.ts must use DOMPurify. While current callers use hardcoded strings, the method is public API and vulnerable to future misuse.

Recommended fix
+import DOMPurify from 'dompurify'
+
 show(html?: string | HTMLElement | HTMLElement[]) {
   if (typeof html === 'string') {
-    this.textElement.innerHTML = html
+    this.textElement.innerHTML = DOMPurify.sanitize(html)
   } else if (html) {
browser_tests/tests/sidebar/workflows.spec.ts (1)

141-154: Early returns may silently pass the test when data is missing.

Using return after expect(exportedWorkflow).toBeDefined() means if exportedWorkflow is unexpectedly undefined, the test will pass without running the slot assertions. This could mask issues where the workflow export fails silently.

Consider using a type guard assertion that throws on failure:

💡 Suggested approach
     expect(exportedWorkflow).toBeDefined()
-    if (!exportedWorkflow) return
-    const nodes = exportedWorkflow.nodes
-    if (!Array.isArray(nodes)) return
+    if (!exportedWorkflow || !Array.isArray(exportedWorkflow.nodes)) {
+      throw new Error('Exported workflow or nodes missing')
+    }
+    const nodes = exportedWorkflow.nodes
browser_tests/tests/featureFlags.spec.ts (1)

334-338: as any usage contradicts PR goals.

This file uses (window as any).__appReadiness which contradicts the PR's objective of removing type suppressions. Consider using the module-level window declaration defined at lines 5-16, or extend the WindowWithMessages type to include __appReadiness.

Suggested fix

Update the addInitScript to use proper typing:

     await newPage.addInitScript(() => {
       // Track when various app components are ready
-      ;(window as any).__appReadiness = {
+      const win = window as Window & typeof globalThis & {
+        __appReadiness: {
+          featureFlagsReceived: boolean
+          apiInitialized: boolean
+          appInitialized: boolean
+        }
+      }
+      win.__appReadiness = {
         featureFlagsReceived: false,
         apiInitialized: false,
         appInitialized: false
       }
src/lib/litegraph/src/LGraphCanvas.ts (2)

7437-7530: Boolean/toggle property editor writes "true"/"false" strings (not booleans).
At Lines 7451-7457 you pass 'true' | 'false' into setValue, but setValue() never converts to boolean—so node.properties[property] becomes a string. Also JSON.parse(String(value)) (Line 7519) can throw and currently isn’t handled.

Proposed fix
-    function inner() {
-      setValue(input?.value)
-    }
+    function inner() {
+      if (input instanceof HTMLInputElement && input.type === 'checkbox') {
+        setValue(input.checked)
+        return
+      }
+      setValue(input?.value)
+    }

     const dirty = () => this.#dirty()

-    function setValue(value: string | number | undefined) {
+    function setValue(value: unknown) {
       if (
-        value !== undefined &&
+        value !== undefined &&
         info?.values &&
         typeof info.values === 'object' &&
-        info.values[value] != undefined
+        info.values[String(value)] != undefined
       ) {
-        value = info.values[value]
+        value = info.values[String(value)]
       }

+      if (type === 'boolean' || type === 'toggle') {
+        if (typeof value === 'string') value = value === 'true'
+        else value = Boolean(value)
+      }
+
       if (typeof node.properties[property] == 'number') {
         value = Number(value)
       }
       if (type == 'array' || type == 'object') {
-        value = JSON.parse(String(value))
+        try {
+          value = JSON.parse(String(value))
+        } catch {
+          // Keep dialog open; avoid mutating on invalid JSON
+          return
+        }
       }
       node.properties[property] = value
       if (node.graph) {
         node.graph._version++
       }
       node.onPropertyChanged?.(property, value)
       options.onclose?.()
       dialog.close()
       dirty()
     }

107-121: Good type widening for search options, but as IShowSearchOptions remains.
IShowSearchOptions is now correctly inclusive of subgraph IO types (Lines 108-111). However Line 851 still forces a cast into showSearchBox, which undermines the PR goal of removing unsafe assertions (and can hide real mismatches).

Suggested direction
-          const linkReleaseContext =
+          const linkReleaseContext: IShowSearchOptions =
             this.linkConnector.state.connectingTo === 'input'
               ? {
-                  node_from: firstLink.node as LGraphNode,
-                  slot_from: firstLink.fromSlot as INodeOutputSlot,
+                  node_from: firstLink.node as LGraphNode,
+                  slot_from: firstLink.fromSlot as INodeOutputSlot,
                   type_filter_in: firstLink.fromSlot.type
                 }
               : {
-                  node_to: firstLink.node as LGraphNode,
-                  slot_to: firstLink.fromSlot as INodeInputSlot,
+                  node_to: firstLink.node as LGraphNode,
+                  slot_to: firstLink.fromSlot as INodeInputSlot,
                   type_filter_out: firstLink.fromSlot.type
                 }

           if ('shiftKey' in e && e.shiftKey) {
             if (this.allow_searchbox) {
-              this.showSearchBox(e, linkReleaseContext as IShowSearchOptions)
+              this.showSearchBox(e, linkReleaseContext)
             }
           }

(If firstLink.node / fromSlot aren’t reliably those types, prefer a runtime guard + early return over widening casts.)

As per coding guidelines, type assertions should be a last resort.

Also applies to: 816-852

🤖 Fix all issues with AI agents
In @browser_tests/fixtures/utils/litegraphUtils.ts:
- Around line 258-264: The widget-access code in getSocketPosition() and
getValue() uses app?.graph whereas the rest of the file (including
getPosition()) uses app?.canvas?.graph; update these methods to fetch the graph
via app?.canvas?.graph, then locate the node by id (node =
canvas.graph.getNodeById(id)), validate node and node.widgets, and index into
node.widgets[index] as before so widget lookup is consistent with getPosition()
and other methods.

In @browser_tests/fixtures/utils/taskHistory.ts:
- Around line 123-139: The reduce accumulator is annotated with a verbose inline
Record type that duplicates the existing TaskOutput shape; change the reducer
initial value type to use TaskOutput instead of the inline Record so the
accumulator is typed by TaskOutput (update the filenames.reduce call and its
initializer to use TaskOutput as the cast/type for the empty object) and ensure
TaskOutput is imported/visible in taskHistory.ts so the code compiles and the
outputs variable retains the same structure.

In @browser_tests/helpers/actionbar.ts:
- Around line 43-54: The setMode method currently silently no-ops when
extMgr.queueSettings is missing; update the page.evaluate callback in setMode to
detect when extMgr.queueSettings is undefined and throw a descriptive error
(e.g., "queueSettings not initialized") so failures are explicit (or
alternatively change setMode to return a boolean indicating success); locate the
setMode function and modify the code inside page.evaluate that references
window['app'], extMgr, and queueSettings to either throw the error or return
true/false to signal success.

In @browser_tests/tests/browserTabTitle.spec.ts:
- Around line 12-19: Extract the repeated inline extensionManager type shape
into a shared helper and replace the three occurrences with calls to it: create
a function like getActiveWorkflowFilename(app: unknown) in
browser_tests/fixtures/utils (or similar), have it cast app to the shape
containing extensionManager.workflow.activeWorkflow.filename and return that
filename, then update the comfyPage.page.evaluate callback to call this helper
instead of duplicating the cast and extMgr logic (references:
comfyPage.page.evaluate, window['app'], extMgr,
workflow?.activeWorkflow?.filename).

In @browser_tests/tests/colorPalette.spec.ts:
- Around line 186-193: The test currently silently no-ops if
addCustomColorPalette is missing; inside the comfyPage.page.evaluate callback
(where you access window['app'] and extMgr via app.extensionManager and call
extMgr.colorPalette?.addCustomColorPalette?.(p) with
customColorPalettes.obsidian_dark), add an explicit runtime assertion that
addCustomColorPalette exists before calling it (e.g., throw a descriptive Error
if undefined) or assert its presence after retrieving extMgr.colorPalette to
ensure the test fails when the method is absent, so the test verifies both
existence and invocation rather than succeeding silently.

In @browser_tests/tests/groupNode.spec.ts:
- Around line 26-28: The test "Is added to node library sidebar" currently
declares an unused parameter named `_comfyPage`; remove the unused parameter or
replace it with an empty destructure (e.g., use `async ({}) =>` or simply `async
() =>`) to follow Playwright conventions and avoid misleading
underscore-prefixed names—update the test signature where it's defined to remove
`_comfyPage`.

In @browser_tests/tests/sidebar/nodeLibrary.spec.ts:
- Around line 268-275: The test currently casts the result of
comfyPage.getSetting to Record and then uses setting?.['foo/'].color which can
throw if setting['foo/'] is undefined; change assertions to consistently use
optional chaining and avoid awaiting synchronous expects: first assert the
nested property exists with expect(setting).toHaveProperty(['foo/','color']) or
assert expect(setting?.['foo/']).toBeDefined(), then use
expect(setting?.['foo/']?.color).not.toBeNull()/not.toBeUndefined()/not.toBe('')
without the unnecessary await; refer to comfyPage.getSetting, setting, and the
'foo/' key when making these edits.

In @browser_tests/tests/sidebar/workflows.spec.ts:
- Around line 183-188: The test currently does an early `return` after
`expect(downloadedContent).toBeDefined()`/`expect(downloadedContentZh).toBeDefined()`,
which can silently skip the final comparison; remove the `if (!downloadedContent
|| !downloadedContentZh) return` guard and proceed to `delete
downloadedContent.id` / `delete downloadedContentZh.id` and
`expect(downloadedContent).toEqual(downloadedContentZh)` so the equality
assertion always runs (or alternatively replace the guard with explicit failure
like `throw new Error(...)`), ensuring `downloadedContent` and
`downloadedContentZh` are asserted non-null before deleting `id` and comparing.

In @browser_tests/tests/subgraph-rename-dialog.spec.ts:
- Around line 28-53: The proposed refactor replacing the inline assertSubgraph
assertion with a generic string constant (SUBGRAPH_ASSERT_INLINE) or creating it
via new Function() breaks TypeScript's type-narrowing and will lose
context-specific assertions; keep the explicit inline assertSubgraph in the test
or, if you must refactor, extract a local helper function inside the same test
file with the same assertion signature(s) (preserving assertions for
inputs/outputs) rather than using SUBGRAPH_ASSERT_INLINE or new Function(), so
Playwright tests retain proper type safety and context-specific narrowing.

In @browser_tests/tests/subgraph.spec.ts:
- Around line 322-337: The inline verbose type on the local variable `graph`
(declared in subgraph.spec.ts) should be extracted to a shared interface to
reduce duplication; create an interface like `SubgraphGraphWithNodes` in
`subgraphUtils.ts` that extends the existing `SubgraphGraph` and adds the
optional `inputNode?: { onPointerDown?: (e: unknown, pointer: unknown,
linkConnector: unknown) => void }` signature, then import and use
`SubgraphGraphWithNodes` to type `graph` (and replace other repeated inline
annotations in this test file) so the code references the named type instead of
repeating the inline annotation.

In @browser_tests/tests/widget.spec.ts:
- Around line 110-123: The in-page guard inside comfyPage.page.evaluate silently
returns when app.graph.nodes[0].widgets[0] is missing, which can mask failures;
change the evaluate callback to throw an error with a clear message (e.g.,
"widget not found") instead of returning so the test fails visibly when
app/graph/widgets aren't initialized; keep the rest of the logic that sets
widget.callback and the subsequent widget.dragHorizontal/expect assertions
unchanged so failures are raised early and clearly (references:
comfyPage.page.evaluate, app.graph.nodes, widget.callback,
widget.dragHorizontal, window.widgetValue).

In @docs/typescript/type-safety.md:
- Around line 386-394: The "Never use `as`" rule in the summary is too absolute
for our repo's testing pattern; update the summary in type-safety.md to add a
brief exception clarifying that test files may use double-cast `as Partial<T> as
T` for constructing partial mocks (i.e., modify the bullet "Never use `as`" to
append "(exception: test files may use `as Partial<T> as T` for partial mocks)")
so contributors understand the intended testing workaround.
- Around line 204-226: Update the guidance that currently claims interfaces have
better performance than type aliases: soften the performance assertion and state
that modern TypeScript has narrowed performance differences; emphasize that
interfaces still offer clearer error messages and support declaration merging
(e.g., for shapes like NodeConfig) while recommending type aliases for unions,
primitives, and tuples. Replace the absolute phrasing with a brief note
mentioning performance differences are minimal in recent TypeScript versions and
highlight error-message clarity and declaration merging as the primary reasons
to prefer interfaces for object shapes.

In @scripts/collect-i18n-node-defs.ts:
- Line 71: The status message currently uses console.warn which signals a
warning level; change the call in collect-i18n-node-defs.ts that logs the
successful collection ("Collected ${nodeDefs.length} node definitions") to an
informational logger such as console.log or console.info so it’s not treated as
an error/CI warning—locate the console.warn invocation and replace it with
console.log (or console.info) to reflect normal successful operation.

In @src/components/common/EditableText.vue:
- Around line 78-85: The optional chaining on el.setSelectionRange is redundant
because after the HTMLInputElement check (el instanceof HTMLInputElement) the
element always has setSelectionRange; update the block around inputRef and
inputValue so that you call el.setSelectionRange(start, end) without the ?.
Ensure you keep the same logic computing fileName, start and end variables and
the early return if el is falsy or not an HTMLInputElement.

In @src/components/common/FormItem.vue:
- Around line 78-81: The current guard in FormItem.vue checks options && typeof
options[0] !== 'string' which treats an empty array as if it had a non-string
first element; update the condition to ensure options is a non-empty array
(e.g., Array.isArray(options) && options.length > 0 && typeof options[0] !==
'string') before setting attrs['optionLabel'] and attrs['optionValue'] so these
are only set when an actual option object exists.

In @src/components/common/TreeExplorerTreeNode.test.ts:
- Around line 64-65: Replace the weakly-typed bracket access to the Badge
component's prop with the type-safe props accessor: use
wrapper.findComponent(Badge).props('value') to retrieve the value (assign to
badgeValue) and then assert expect(String(badgeValue)).toBe('3'); this keeps the
removal of @ts-expect-error while improving typing and IDE support for the test.

In @src/components/dialog/content/MissingCoreNodesMessage.test.ts:
- Around line 78-80: Replace the unsafe cast of the mock with the
partial-then-full pattern: where you call
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore as unknown
as ReturnType<typeof useSystemStatsStore>), change the cast to
mockSystemStatsStore as Partial<ReturnType<typeof useSystemStatsStore>> as
ReturnType<typeof useSystemStatsStore>; this makes the incomplete mock
implementation explicit while preserving the expected ReturnType for
useSystemStatsStore.

In @src/components/graph/GraphCanvas.vue:
- Around line 431-432: The null-check on canvasRef.value silently returns
instead of surfacing an unexpected missing canvas; update the onMounted
initialization (where canvasRef and comfyApp.setup are used) to assert or log an
error when canvasRef.value is null so we fail loudly during development — e.g.,
throw or call console.error/processLogger.error with context including the
component name and that comfyApp.setup was skipped, then only call
comfyApp.setup(canvasRef.value) when canvasRef.value is present; reference
canvasRef, comfyApp.setup and the onMounted initialization block to locate where
to add the assertion/log.

In @src/components/topbar/CurrentUserButton.vue:
- Around line 65-66: The component currently exposes the raw popover ref via
defineExpose({ popover, closePopover }); instead expose only imperative methods
(e.g., closePopover and the existing togglePopover/openPopover methods) to avoid
leaking the Popover instance: remove popover from defineExpose and add any
public action methods (togglePopover/openPopover) so external code can control
the popover without direct ref access; also update tests to call togglePopover
(or the exposed method) instead of accessing popover.toggle.

In @src/composables/maskeditor/useCanvasManager.test.ts:
- Around line 39-73: Replace the module-level mutable mockStore with a hoisted
factory using vi.hoisted: wrap creation of mockStore and the getter functions
(getImgCanvas, getMaskCanvas, getRgbCanvas, getImgCtx, getMaskCtx, getRgbCtx,
getCanvasBackground) inside vi.hoisted(() => { ... }) so the mockStore is
created per test scope and the getters close over that local store; move the
initial null-initialized mockStore object and the existing null-guarding getter
implementations into the hoisted callback and export the getters and mockStore
from that vi.hoisted result.

In @src/extensions/core/contextMenuFilter.ts:
- Around line 38-51: Add a defensive null/undefined guard around
LGraphCanvas.active_canvas before accessing .current_node in the
requestAnimationFrame callback: check that LGraphCanvas.active_canvas is truthy
(and optionally that .current_node exists) and bail early if not, so the
subsequent access to current_node, its widgets and the combo-widget matching
logic (references: LGraphCanvas.active_canvas, current_node, isComboWidget,
widgets) cannot cause a runtime null access.

In @src/extensions/core/groupNode.ts:
- Around line 669-678: The mergeIfValid call uses brittle casts
(Parameters<typeof mergeIfValid>[n]) for output, targetWidget and
primitiveConfig because widget config types are dynamic; add a concise inline
comment above the const output and the mergeIfValid invocation explaining why
the casts are required (e.g., dynamic runtime widget shape, compiler cannot
infer unions) and mark them as intentional to avoid accidental cleanup by future
maintainers, keeping the casts but clarifying their necessity and linking to the
mergeIfValid function name for context.

In @src/extensions/core/groupNodeTypes.ts:
- Around line 53-57: PartialLinkInfo currently merely aliases ILinkRouting which
provides no type-level distinction; either add a discriminating property (e.g.,
a readonly discriminant like "kind" or "__partial" on the PartialLinkInfo
interface) so TypeScript can differentiate partial/synthetic links from full
ILinkRouting, or keep it purely semantic and add a JSDoc on PartialLinkInfo
explaining the intended difference and why it aliases ILinkRouting; update
usages such as the group node getInputLink override to rely on the discriminant
if you choose the former.

In @src/extensions/core/webcamCapture.ts:
- Around line 40-45: The promise in resolveVideo is being resolved both by a
fallback setTimeout and the 'loadedmetadata' listener, so update the logic to
avoid redundant resolution: when attaching the 'loadedmetadata' handler for the
video (the listener that calls resolveVideo(video)), use a one-time listener
(e.g., pass the once option) or explicitly remove the listener after it fires,
and make the timeout handler clear the timeout if the event already fired (or
clear the timeout inside the event handler) so only one path calls resolveVideo;
reference the existing video.addEventListener('loadedmetadata', ...) and the
setTimeout(...) fallback when making this change.

In @src/extensions/core/widgetInputs.ts:
- Around line 111-117: The code assigns comboWidget.value = newValues[0] without
guarding against newValues being empty; update the logic in the block that
handles Array.isArray(rawValues) (where comboWidget.options.values is set) to
check newValues.length > 0 before assigning newValues[0]; if newValues is empty,
set comboWidget.value to a safe default (e.g., '' or null) and still invoke
comboWidget.callback with that safe value (or skip the callback if that is more
appropriate) so you never assign undefined to comboWidget.value.

In @src/lib/litegraph/src/LGraph.ts:
- Around line 705-718: The priority extraction uses '??' but the preceding
ternary always yields a number (0), so the fallback is never reached; update the
logic in computing priorityA and priorityB (variables ctorA, ctorB, A.priority,
B.priority) to nest the ternaries or otherwise chain checks so that if the
constructor has a numeric priority use it, else if the instance has a numeric
priority use that, else use 0 (i.e., remove the '??' and make the ternary return
the instance priority or 0 as the final fallback).

In @src/lib/litegraph/src/LGraphCanvas.ts:
- Around line 3682-3684: The keyboard event early-return currently only skips
when targetEl instanceof HTMLInputElement, which lets events leak when focus is
inside a textarea or contenteditable region; update the check in the keyboard
handler(s) to also return early when targetEl is an HTMLTextAreaElement or when
targetEl (or any of its ancestors) has isContentEditable === true — i.e.,
replace/extend the instanceof test on targetEl with a guard that checks
HTMLInputElement OR HTMLTextAreaElement OR walks up targetEl.parentNode to
detect isContentEditable, and apply the same fix to the other handler block
referenced around lines 3720-3732 so both keyboard handling sites use the same
focus-sensitive guard.
- Around line 165-172: checkPanels() is closing panels because newly created
panels lack panel.graph so the comparison panel.graph != this.graph evaluates
true; fix by ensuring any panel created for this canvas has its graph set—assign
panel.graph = this.graph when creating panels (in createPanel()) and/or when
showing node panels (e.g., in showShowNodePanel() after creating the panel set
panel.graph = this.graph) so checkPanels() recognizes them as belonging to this
graph.

In @src/lib/litegraph/src/LiteGraphGlobal.ts:
- Around line 838-874: The removal logic duplicates calls and returns values
incorrectly: consolidate into a single removal per event and avoid returning the
result (this is a void function). Specifically, for events in
pointerAndMouseEvents call oDOM.removeEventListener(this.pointerevents_method +
sEvent, fCall, capture) once when this.pointerevents_method is 'pointer' or
'mouse'; for events in pointerOnlyEvents call
oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture)
only when this.pointerevents_method is 'pointer'; otherwise call
oDOM.removeEventListener(sEvent, fCall, capture). Remove any duplicate calls and
any return statements around oDOM.removeEventListener; reference
pointerAndMouseEvents, pointerOnlyEvents, this.pointerevents_method,
oDOM.removeEventListener, sEvent, fCall, capture to locate and update the code.

In @src/lib/litegraph/src/node/NodeSlot.test.ts:
- Around line 16-27: The describe block title is misleading because it currently
reads 'outputAsSerialisable' but the tests exercise both outputAsSerialisable
and inputAsSerialisable; rename the describe to something inclusive like 'slot
serialization' or split into two describes (one for outputAsSerialisable and one
for inputAsSerialisable) so each unit under test is clearly grouped; update the
describe string and, if splitting, move the existing tests into the appropriate
new describe blocks while keeping the test bodies (they reference
outputAsSerialisable and inputAsSerialisable) unchanged.

In @src/lib/litegraph/src/subgraph/SubgraphSerialization.test.ts:
- Around line 288-301: Add a brief inline comment explaining that the deliberate
use of "as unknown as ExportedSubgraph" on the extendedFormat test object is
intentional to simulate a future schema/version and to validate deserialization
resilience; update the test near the extendedFormat declaration (the variable
named extendedFormat used when calling new Subgraph(new LGraph(),
extendedFormat)) to state that this type assertion is an acceptable exception to
normal rules for forward-compatibility testing.

In
@src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts:
- Around line 26-27: The helper findFileComboWidget currently assumes
node.widgets exists and that find() returns a value by using node.widgets! and
casting to IComboWidget; make it robust by adding null guards: inside
findFileComboWidget (and/or its callers) check that node.widgets is defined
before calling find, have findFileComboWidget return IComboWidget | undefined
(remove the unsafe cast), and at the call site where fileComboWidget is used
(e.g., where imageInputName was validated by isImageUploadInput) explicitly
check for undefined and throw a clear Error like `Widget "${imageInputName}" not
found on node` before proceeding.

In @src/scripts/metadata/avif.ts:
- Line 193: The return type on the function signature redundantly appends "|
null" to IsobmffBoxContentRange (which already includes null); update the
function declaration so the return type is simply IsobmffBoxContentRange (remove
the extra "| null") — look for the signature that ends with "):
IsobmffBoxContentRange | null {" and change it to use only
IsobmffBoxContentRange.

In @src/scripts/metadata/flac.ts:
- Around line 38-43: The reader.onload handler currently resolves with an empty
object when event.target?.result is not an ArrayBuffer; add an error log there
for consistency with the other signature-check branch: inside reader.onload
(function assigned to reader.onload) when the else branch is taken, call the
project's logger (e.g., processLogger.error or console.error) to record that
FileReader returned a non-ArrayBuffer result before calling r({}); keep using
getFromFlacBuffer and r as-is so only the unexpected-result path gains
observability.

In @src/scripts/ui/components/button.ts:
- Around line 31-39: Properties on ComfyButton (icon, overIcon, iconSize,
content, tooltip, classList, hidden, enabled, action) are declared with
definite-assignment (!) but the corresponding ComfyButtonProps are optional so
these can be undefined at runtime; update each property declaration to use
explicit union types (e.g., icon: string | undefined, overIcon: string |
undefined, iconSize: number | undefined, content: string | HTMLElement |
undefined, tooltip: string | undefined, classList: ClassList | undefined,
hidden: boolean | undefined, enabled: boolean | undefined, action: ((e: Event,
btn: ComfyButton) => void) | undefined) and remove the unnecessary ! assertions
so the prop() assignments (the prop(...) calls referenced in your comment)
reflect possible undefined values and keep existing runtime fallbacks intact.

In @src/scripts/ui/draggableList.ts:
- Around line 126-130: The code reads touch coordinates with e.touches[0]
without verifying the touch list; update the pointer extraction in the handler
that sets this.pointerStartX and this.pointerStartY to first check that
e.touches exists and e.touches.length > 0 (or e.changedTouches for certain
events) before accessing index 0, and fall back to using e.clientX/e.clientY or
abort the handler if no touch is available; update the logic around the
clientX/clientY extraction so it safely handles empty touch lists and avoids
runtime exceptions.

In @src/scripts/ui/toggleSwitch.ts:
- Around line 3-8: Export the ToggleSwitchItem interface so external consumers
can type the public API (e.g., the onChange callback) by adding the export
modifier to the existing interface declaration (ToggleSwitchItem) and update any
local references or public type re-exports if you have a barrel export so the
type is available from the package entry points; ensure consumers can import
ToggleSwitchItem where they implement the onChange handler.
- Around line 29-35: The callback currently casts items[index] to
ToggleSwitchItem unsafely because the original items array isn't normalized;
normalize the items array once before any access (e.g., create normalizedItems =
items.map(...) using the same normalization logic currently inside map) and then
use normalizedItems[index] and normalizedItems[selectedIndex] in the onChange
call (and anywhere updateSelected reads items) so the callback always receives a
proper ToggleSwitchItem instead of relying on type assertions.

In @src/scripts/widgets.ts:
- Around line 10-27: The helper type ComboValuesType and function
getComboValuesArray are defined between import blocks; move both the
ComboValuesType alias and the getComboValuesArray function so they appear after
all import statements (i.e., below the final import grouping), ensuring you do
not split the top import section—also keep INumericWidget and other type imports
together at the top with the other imports so only the helper type and function
are relocated.

In @src/types/litegraph-augmentation.d.ts:
- Around line 116-121: The call site is casting recreate as returning void and
not awaiting the new Promise-returning signature; update the caller where you
see the cast `(node as LGraphNode & { recreate?: () => void }).recreate?.()` to
cast to `() => Promise<LGraphNode | null>` and await the call (e.g. `const
newNode = await (node as LGraphNode & { recreate?: () => Promise<LGraphNode |
null> }).recreate?.()`), then handle the possibility that `newNode` is null
before proceeding. Ensure the awaited result is used or guarded appropriately to
satisfy the updated `recreate?(): Promise<LGraphNode | null>` contract.

In @src/utils/vintageClipboard.ts:
- Around line 89-95: Add a lightweight type assertion to the JSON.parse result
to give the IDE/typechecker shape info for deserialised.nodes and
deserialised.links: where deserialised is assigned (currently via
JSON.parse(data) near the graph.beforeChange() call), assert it to an
appropriate legacy shape (e.g., an object with nodes and links arrays) so
subsequent accesses get type inference; no runtime validation required since
this is deprecated handling—just change the JSON.parse assignment to include the
type assertion for deserialised.

In
@src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts:
- Line 289: Replace the inconsistent ref mutation using Object.assign on
workflowPacksRef with the idiomatic direct .value assignment: set
workflowPacksRef.value = mockWorkflowPacks (or [] as used elsewhere) instead of
Object.assign(workflowPacksRef, { value: mockWorkflowPacks }); if you used
Object.assign to silence a type error, adjust the ref typing so direct
assignment to workflowPacksRef.value is allowed.
- Around line 12-28: The helper createMockNodeDefStore should use the
PartialNodeDefStore type for consistency: change its signature to return
PartialNodeDefStore (instead of ReturnType<typeof useNodeDefStore>) and type the
local nodeDefsByName as PartialNodeDefStore['nodeDefsByName']; return the object
cast as PartialNodeDefStore. Keep references to PartialNodeDefStore,
createMockNodeDefStore, and useNodeDefStore so the intent and typings are clear.

In @src/workbench/utils/nodeDefOrderingUtil.ts:
- Around line 4-6: The interface HasInputOrder uses a loose input_order:
Record<string, string[]> which misses IDE/type safety for the known properties;
change it to a more specific type exposing the expected keys so accesses to
.required and .optional are typed (e.g., replace input_order?:
Record<string,string[]> with a typed shape that includes required?: string[] and
optional?: string[]), update any code using HasInputOrder to rely on those typed
properties (references: HasInputOrder, input_order, .required, .optional).

Comment on lines 123 to 139
return filenames.reduce(
(outputs, filename, i) => {
const nodeId = `${i + 1}`
outputs[nodeId] = {
[filetype]: [{ filename, subfolder: '', type: 'output' }]
}
const contentType = getContentType(filename, filetype)
this.outputContentTypes.set(filename, contentType)
return outputs
},
{} as Record<
string,
{
[key: string]: { filename: string; subfolder: string; type: string }[]
}
>
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using TaskOutput directly as the accumulator type.

The inline type annotation duplicates the structure that's likely already defined in TaskOutput. If TaskOutput is compatible with an empty object initializer, you could simplify:

♻️ Suggested simplification
     return filenames.reduce(
       (outputs, filename, i) => {
         const nodeId = `${i + 1}`
         outputs[nodeId] = {
           [filetype]: [{ filename, subfolder: '', type: 'output' }]
         }
         const contentType = getContentType(filename, filetype)
         this.outputContentTypes.set(filename, contentType)
         return outputs
       },
-      {} as Record<
-        string,
-        {
-          [key: string]: { filename: string; subfolder: string; type: string }[]
-        }
-      >
+      {} as TaskOutput
     )

If TaskOutput has additional required properties that make this incompatible, the current verbose annotation is acceptable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return filenames.reduce(
(outputs, filename, i) => {
const nodeId = `${i + 1}`
outputs[nodeId] = {
[filetype]: [{ filename, subfolder: '', type: 'output' }]
}
const contentType = getContentType(filename, filetype)
this.outputContentTypes.set(filename, contentType)
return outputs
},
{} as Record<
string,
{
[key: string]: { filename: string; subfolder: string; type: string }[]
}
>
)
return filenames.reduce(
(outputs, filename, i) => {
const nodeId = `${i + 1}`
outputs[nodeId] = {
[filetype]: [{ filename, subfolder: '', type: 'output' }]
}
const contentType = getContentType(filename, filetype)
this.outputContentTypes.set(filename, contentType)
return outputs
},
{} as TaskOutput
)
🤖 Prompt for AI Agents
In @browser_tests/fixtures/utils/taskHistory.ts around lines 123 - 139, The
reduce accumulator is annotated with a verbose inline Record type that
duplicates the existing TaskOutput shape; change the reducer initial value type
to use TaskOutput instead of the inline Record so the accumulator is typed by
TaskOutput (update the filenames.reduce call and its initializer to use
TaskOutput as the cast/type for the empty object) and ensure TaskOutput is
imported/visible in taskHistory.ts so the code compiles and the outputs variable
retains the same structure.

Comment on lines 43 to 54
public async setMode(mode: AutoQueueMode) {
await this.page.evaluate((mode) => {
window['app'].extensionManager.queueSettings.mode = mode
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
queueSettings?: { mode: string }
}
if (extMgr.queueSettings) {
extMgr.queueSettings.mode = mode
}
}, mode)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Silent no-op when queueSettings is missing may mask test setup issues.

The guard on line 50 silently skips the assignment when queueSettings is undefined. If a test calls setMode expecting it to succeed but queueSettings doesn't exist, the test may pass incorrectly or exhibit confusing behavior.

Consider throwing an error for consistency with the app guard, or at minimum returning a boolean to indicate success:

🔧 Suggested improvement
  public async setMode(mode: AutoQueueMode) {
    await this.page.evaluate((mode) => {
      const app = window['app']
      if (!app) throw new Error('App not initialized')
      const extMgr = app.extensionManager as {
        queueSettings?: { mode: string }
      }
-     if (extMgr.queueSettings) {
-       extMgr.queueSettings.mode = mode
-     }
+     if (!extMgr.queueSettings) throw new Error('queueSettings not initialized')
+     extMgr.queueSettings.mode = mode
    }, mode)
  }
🤖 Prompt for AI Agents
In @browser_tests/helpers/actionbar.ts around lines 43 - 54, The setMode method
currently silently no-ops when extMgr.queueSettings is missing; update the
page.evaluate callback in setMode to detect when extMgr.queueSettings is
undefined and throw a descriptive error (e.g., "queueSettings not initialized")
so failures are explicit (or alternatively change setMode to return a boolean
indicating success); locate the setMode function and modify the code inside
page.evaluate that references window['app'], extMgr, and queueSettings to either
throw the error or return true/false to signal success.

Comment on lines 12 to 19
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
workflow?: { activeWorkflow?: { filename?: string } }
}
return extMgr.workflow?.activeWorkflow?.filename
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting the repeated type shape into a shared helper.

The inline type assertion for extensionManager is repeated three times in this file with slight variations. While this works correctly, consider extracting a helper function in a shared test utilities file (e.g., browser_tests/fixtures/utils/) to reduce duplication and improve maintainability.

// Example helper in browser_tests/fixtures/utils/appUtils.ts
export function getActiveWorkflowFilename(app: unknown): string | undefined {
  const extMgr = (app as { extensionManager?: { workflow?: { activeWorkflow?: { filename?: string } } } }).extensionManager
  return extMgr?.workflow?.activeWorkflow?.filename
}
🤖 Prompt for AI Agents
In @browser_tests/tests/browserTabTitle.spec.ts around lines 12 - 19, Extract
the repeated inline extensionManager type shape into a shared helper and replace
the three occurrences with calls to it: create a function like
getActiveWorkflowFilename(app: unknown) in browser_tests/fixtures/utils (or
similar), have it cast app to the shape containing
extensionManager.workflow.activeWorkflow.filename and return that filename, then
update the comfyPage.page.evaluate callback to call this helper instead of
duplicating the cast and extMgr logic (references: comfyPage.page.evaluate,
window['app'], extMgr, workflow?.activeWorkflow?.filename).

Comment on lines 186 to 193
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
}
extMgr.colorPalette?.addCustomColorPalette?.(p)
}, customColorPalettes.obsidian_dark)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Improved type safety with explicit null guard and type narrowing.

The guarded access pattern with an explicit throw if app is undefined is cleaner and provides better error messages than silent failures. The inline type annotation for extMgr with optional chaining safely navigates the nested optional properties.

One minor consideration: if addCustomColorPalette is undefined, the test will silently succeed without actually calling the method. If the intent is to test that the method exists and works, consider adding an assertion:

💡 Optional: Add assertion for method existence
       const extMgr = app.extensionManager as {
         colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
       }
+      if (!extMgr.colorPalette?.addCustomColorPalette) {
+        throw new Error('addCustomColorPalette method not found')
+      }
       extMgr.colorPalette?.addCustomColorPalette?.(p)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
}
extMgr.colorPalette?.addCustomColorPalette?.(p)
}, customColorPalettes.obsidian_dark)
await comfyPage.page.evaluate((p) => {
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
}
if (!extMgr.colorPalette?.addCustomColorPalette) {
throw new Error('addCustomColorPalette method not found')
}
extMgr.colorPalette?.addCustomColorPalette?.(p)
}, customColorPalettes.obsidian_dark)
🤖 Prompt for AI Agents
In @browser_tests/tests/colorPalette.spec.ts around lines 186 - 193, The test
currently silently no-ops if addCustomColorPalette is missing; inside the
comfyPage.page.evaluate callback (where you access window['app'] and extMgr via
app.extensionManager and call extMgr.colorPalette?.addCustomColorPalette?.(p)
with customColorPalettes.obsidian_dark), add an explicit runtime assertion that
addCustomColorPalette exists before calling it (e.g., throw a descriptive Error
if undefined) or assert its presence after retrieving extMgr.colorPalette to
ensure the test fails when the method is absent, so the test verifies both
existence and invocation rather than succeeding silently.

Comment on lines 26 to 28
test('Is added to node library sidebar', async ({
comfyPage: _comfyPage
}) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using a more meaningful parameter name or removing it entirely.

The underscore-prefixed _comfyPage indicates an unused parameter. If the test doesn't use comfyPage, consider destructuring an empty object or using the Playwright pattern of just {}.

Optional: Remove unused parameter
-    test('Is added to node library sidebar', async ({
-      comfyPage: _comfyPage
-    }) => {
+    test('Is added to node library sidebar', async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('Is added to node library sidebar', async ({
comfyPage: _comfyPage
}) => {
test('Is added to node library sidebar', async () => {
🤖 Prompt for AI Agents
In @browser_tests/tests/groupNode.spec.ts around lines 26 - 28, The test "Is
added to node library sidebar" currently declares an unused parameter named
`_comfyPage`; remove the unused parameter or replace it with an empty
destructure (e.g., use `async ({}) =>` or simply `async () =>`) to follow
Playwright conventions and avoid misleading underscore-prefixed names—update the
test signature where it's defined to remove `_comfyPage`.

Comment on lines 116 to 121
/**
* @deprecated Widget to socket conversion is no longer necessary as they co-exist now.
* This method is a no-op stub for backward compatibility with extensions.
*/
convertWidgetToInput?(): boolean
recreate?(): Promise<LGraphNode | null>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n --type=ts --type=js -C3 '\.recreate\s*\('

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

# Also check for recreate without the dot prefix (direct function calls, imports, etc.)
rg -n --type=ts --type=js -C2 'recreate' | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 6936


🏁 Script executed:

sed -n '575,590p' src/extensions/core/groupNodeManage.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 607


🏁 Script executed:

sed -n '1120,1135p' src/extensions/core/groupNode.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 500


Fix Promise handling and type casting in recreate() caller.

The type change to Promise<LGraphNode | null> is correct and well-documented, but there is a critical bug in src/extensions/core/groupNodeManage.ts:581 where the caller casts recreate as returning void and does not await the Promise:

;(node as LGraphNode & { recreate?: () => void }).recreate?.()

This violates the new type contract. The call must await the result and handle the null case:

await (node as LGraphNode & { recreate?: () => Promise<LGraphNode | null> }).recreate?.()
🤖 Prompt for AI Agents
In @src/types/litegraph-augmentation.d.ts around lines 116 - 121, The call site
is casting recreate as returning void and not awaiting the new Promise-returning
signature; update the caller where you see the cast `(node as LGraphNode & {
recreate?: () => void }).recreate?.()` to cast to `() => Promise<LGraphNode |
null>` and await the call (e.g. `const newNode = await (node as LGraphNode & {
recreate?: () => Promise<LGraphNode | null> }).recreate?.()`), then handle the
possibility that `newNode` is null before proceeding. Ensure the awaited result
is used or guarded appropriately to satisfy the updated `recreate?():
Promise<LGraphNode | null>` contract.

Comment on lines 89 to 95
if (!graph) return

canvas.emitBeforeChange()
try {
// @ts-expect-error fixme ts strict error
graph.beforeChange()

const deserialised = JSON.parse(data)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good null guard addition.

The early return on line 89-90 properly prevents accessing methods on an undefined graph.

Consider adding a type assertion for the parsed data to improve type safety within the function:

-    const deserialised = JSON.parse(data)
+    const deserialised = JSON.parse(data) as VintageSerialisable

This would provide type inference for deserialised.nodes and deserialised.links accesses below. Since this is a deprecated function handling legacy format, runtime validation may be overkill, but the type assertion costs nothing and improves IDE support.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!graph) return
canvas.emitBeforeChange()
try {
// @ts-expect-error fixme ts strict error
graph.beforeChange()
const deserialised = JSON.parse(data)
if (!graph) return
canvas.emitBeforeChange()
try {
graph.beforeChange()
const deserialised = JSON.parse(data) as VintageSerialisable
🤖 Prompt for AI Agents
In @src/utils/vintageClipboard.ts around lines 89 - 95, Add a lightweight type
assertion to the JSON.parse result to give the IDE/typechecker shape info for
deserialised.nodes and deserialised.links: where deserialised is assigned
(currently via JSON.parse(data) near the graph.beforeChange() call), assert it
to an appropriate legacy shape (e.g., an object with nodes and links arrays) so
subsequent accesses get type inference; no runtime validation required since
this is deprecated handling—just change the JSON.parse assignment to include the
type assertion for deserialised.

Comment on lines 12 to 28
type PartialNodeDefStore = Pick<
ReturnType<typeof useNodeDefStore>,
'nodeDefsByName'
>
type PartialManagerStore = Pick<
ReturnType<typeof useComfyManagerStore>,
'isPackInstalled'
>

function createMockNodeDefStore(
names: string[]
): ReturnType<typeof useNodeDefStore> {
const nodeDefsByName = Object.fromEntries(
names.map((name) => [name, { name } as ComfyNodeDefImpl])
)
return { nodeDefsByName } as ReturnType<typeof useNodeDefStore>
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good use of Pick<> for partial store types and centralized mock factory.

The PartialNodeDefStore and PartialManagerStore types provide a clear contract for what the tests require. The createMockNodeDefStore helper centralizes mock creation, reducing duplication.

Minor note: The helper could use PartialNodeDefStore internally for consistency:

🔧 Optional refinement
 function createMockNodeDefStore(
   names: string[]
 ): ReturnType<typeof useNodeDefStore> {
   const nodeDefsByName = Object.fromEntries(
     names.map((name) => [name, { name } as ComfyNodeDefImpl])
   )
-  return { nodeDefsByName } as ReturnType<typeof useNodeDefStore>
+  const partial: PartialNodeDefStore = { nodeDefsByName }
+  return partial as ReturnType<typeof useNodeDefStore>
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type PartialNodeDefStore = Pick<
ReturnType<typeof useNodeDefStore>,
'nodeDefsByName'
>
type PartialManagerStore = Pick<
ReturnType<typeof useComfyManagerStore>,
'isPackInstalled'
>
function createMockNodeDefStore(
names: string[]
): ReturnType<typeof useNodeDefStore> {
const nodeDefsByName = Object.fromEntries(
names.map((name) => [name, { name } as ComfyNodeDefImpl])
)
return { nodeDefsByName } as ReturnType<typeof useNodeDefStore>
}
type PartialNodeDefStore = Pick<
ReturnType<typeof useNodeDefStore>,
'nodeDefsByName'
>
type PartialManagerStore = Pick<
ReturnType<typeof useComfyManagerStore>,
'isPackInstalled'
>
function createMockNodeDefStore(
names: string[]
): ReturnType<typeof useNodeDefStore> {
const nodeDefsByName = Object.fromEntries(
names.map((name) => [name, { name } as ComfyNodeDefImpl])
)
const partial: PartialNodeDefStore = { nodeDefsByName }
return partial as ReturnType<typeof useNodeDefStore>
}
🤖 Prompt for AI Agents
In
@src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts
around lines 12 - 28, The helper createMockNodeDefStore should use the
PartialNodeDefStore type for consistency: change its signature to return
PartialNodeDefStore (instead of ReturnType<typeof useNodeDefStore>) and type the
local nodeDefsByName as PartialNodeDefStore['nodeDefsByName']; return the object
cast as PartialNodeDefStore. Keep references to PartialNodeDefStore,
createMockNodeDefStore, and useNodeDefStore so the intent and typings are clear.

// Update workflow packs
// @ts-expect-error - mockWorkflowPacks is a simplified version without full WorkflowPack interface.
workflowPacksRef.value = mockWorkflowPacks
Object.assign(workflowPacksRef, { value: mockWorkflowPacks })
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Inconsistent ref mutation pattern.

This uses Object.assign to update the ref's value, but line 314 in the same describe block uses direct assignment (workflowPacksRef.value = []). The direct assignment is the idiomatic Vue pattern for updating refs.

If both approaches work, prefer consistency:

🔧 Suggested fix
-      Object.assign(workflowPacksRef, { value: mockWorkflowPacks })
+      workflowPacksRef.value = mockWorkflowPacks

If this Object.assign approach was needed to resolve a type error, please clarify. Otherwise, direct assignment should be used for consistency with the rest of the file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object.assign(workflowPacksRef, { value: mockWorkflowPacks })
workflowPacksRef.value = mockWorkflowPacks
🤖 Prompt for AI Agents
In
@src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts
at line 289, Replace the inconsistent ref mutation using Object.assign on
workflowPacksRef with the idiomatic direct .value assignment: set
workflowPacksRef.value = mockWorkflowPacks (or [] as used elsewhere) instead of
Object.assign(workflowPacksRef, { value: mockWorkflowPacks }); if you used
Object.assign to silence a type error, adjust the ref typing so direct
assignment to workflowPacksRef.value is allowed.

Comment on lines 4 to 6
interface HasInputOrder {
input_order?: Record<string, string[]>
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider a more specific type for input_order.

The code at lines 28 and 38 specifically accesses .required and .optional properties. A more precise interface would provide better IDE autocomplete and catch typos at compile time:

 interface HasInputOrder {
-  input_order?: Record<string, string[]>
+  input_order?: {
+    required?: string[]
+    optional?: string[]
+  }
 }

That said, if extensibility for future input categories is intended, the current Record<string, string[]> is acceptable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface HasInputOrder {
input_order?: Record<string, string[]>
}
interface HasInputOrder {
input_order?: {
required?: string[]
optional?: string[]
}
}
🤖 Prompt for AI Agents
In @src/workbench/utils/nodeDefOrderingUtil.ts around lines 4 - 6, The interface
HasInputOrder uses a loose input_order: Record<string, string[]> which misses
IDE/type safety for the known properties; change it to a more specific type
exposing the expected keys so accesses to .required and .optional are typed
(e.g., replace input_order?: Record<string,string[]> with a typed shape that
includes required?: string[] and optional?: string[]), update any code using
HasInputOrder to rely on those typed properties (references: HasInputOrder,
input_order, .required, .optional).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines 258 to 264
const app = window.app
if (!app?.graph) throw new Error('App not initialized')
const node = app.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (!node.widgets) throw new Error(`Node ${id} has no widgets.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
Copy link
Contributor

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 graph access patterns in the litegraphUtils file
rg -n "app\.(graph|canvas\.graph)" browser_tests/fixtures/utils/litegraphUtils.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 701


🏁 Script executed:

#!/bin/bash
# Get context around line 314 to understand the other app.graph usage
sed -n '310,320p' browser_tests/fixtures/utils/litegraphUtils.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 568


🏁 Script executed:

#!/bin/bash
# Get more context to see the method names for both app.graph usages
sed -n '250,270p' browser_tests/fixtures/utils/litegraphUtils.ts
echo "---"
sed -n '305,325p' browser_tests/fixtures/utils/litegraphUtils.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1563


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or patterns suggesting why widget operations might need app.graph specifically
# Also check what methods nearby use for comparison
sed -n '185,240p' browser_tests/fixtures/utils/litegraphUtils.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2018


Align widget access with the app?.canvas?.graph pattern used throughout the file.

The getSocketPosition() and getValue() methods use app?.graph, but the getPosition() method in the same class uses app?.canvas?.graph. All other methods in the file consistently use app?.canvas?.graph (10 occurrences vs. 2 using app?.graph). For consistency, both widget methods should use the same pattern.

🤖 Prompt for AI Agents
In @browser_tests/fixtures/utils/litegraphUtils.ts around lines 258 - 264, The
widget-access code in getSocketPosition() and getValue() uses app?.graph whereas
the rest of the file (including getPosition()) uses app?.canvas?.graph; update
these methods to fetch the graph via app?.canvas?.graph, then locate the node by
id (node = canvas.graph.getNodeById(id)), validate node and node.widgets, and
index into node.widgets[index] as before so widget lookup is consistent with
getPosition() and other methods.

Comment on lines +313 to +317
const finalizedDef = this.nodeDef as ComfyNodeDef & {
[GROUP]: GroupNodeConfig
}
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, finalizedDef)
useNodeDefStore().addNodeDef(finalizedDef)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and inspect registerNodeDef and addNodeDef implementations
rg -n -A 15 "registerNodeDef|addNodeDef" --type=ts src/scripts/app.ts src/stores/nodeDefStore.ts 2>/dev/null | head -80

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 5082


🏁 Script executed:

#!/bin/bash
# Find the type definitions for ComfyNodeDef, GroupNodeInputsSpec, and GroupNodeOutputType
rg -n "type ComfyNodeDef|interface ComfyNodeDef|type GroupNodeInputsSpec|type GroupNodeOutputType" --type=ts src/

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 482


🏁 Script executed:

#!/bin/bash
# Get context around lines 313-317 in groupNode.ts
sed -n '300,330p' src/extensions/core/groupNode.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 986


🏁 Script executed:

#!/bin/bash
# Find ComfyNodeDefV1 definition
rg -n "type ComfyNodeDefV1|interface ComfyNodeDefV1" --type=ts src/

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Find GroupNodeConfig definition
rg -n "type GroupNodeConfig|interface GroupNodeConfig" --type=ts src/

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Find registerNodeDef implementation in app.ts
rg -n -A 15 "registerNodeDef\s*\(" --type=ts src/scripts/app.ts | head -60

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1608


🏁 Script executed:

#!/bin/bash
# Check the type of this.nodeDef in groupNode.ts
sed -n '1,100p' src/extensions/core/groupNode.ts | rg -n "nodeDef|class|constructor"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 202


🏁 Script executed:

#!/bin/bash
# Find where ComfyNodeDefV1 is defined or imported
rg -n "ComfyNodeDefV1" --type=ts src/ | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1806


🏁 Script executed:

#!/bin/bash
# Check the top of groupNode.ts for imports and class definition
head -80 src/extensions/core/groupNode.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2583


🏁 Script executed:

#!/bin/bash
# Search for GroupNodeConfig in the entire src directory
rg -n "GroupNodeConfig" --type=ts src/ | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1937


🏁 Script executed:

#!/bin/bash
# Look at the ComfyNodeDef schema definitions more closely
cat src/schemas/nodeDefSchema.ts | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3407


🏁 Script executed:

#!/bin/bash
# Check GroupNodeConfig class definition to see how this.nodeDef is created/assigned
sed -n '234,350p' src/extensions/core/groupNode.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3624


🏁 Script executed:

#!/bin/bash
# Check how this.nodeDef is initialized in GroupNodeConfig
rg -n -B 5 -A 10 "this\.nodeDef\s*=" src/extensions/core/groupNode.ts | head -80

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 643


Verify that downstream consumers properly handle the custom input and output shapes.

The cast to ComfyNodeDef & { [GROUP]: GroupNodeConfig } masks a type incompatibility. This nodeDef is actually typed with Omit<ComfyNodeDef, 'input' | 'output'> & { input: GroupNodeInputsSpec; output: GroupNodeOutputType[] } (lines 246–256), which uses custom input and output shapes rather than the standard ComfyNodeDef structures. When passed to registerNodeDef and addNodeDef, which expect standard ComfyNodeDefV1 shapes, ensure these consumers handle the custom input/output formats correctly or refactor to avoid the unsafe type cast.

Comment on lines 669 to 678
const output = { widget: primitiveConfig } as unknown as Parameters<
typeof mergeIfValid
>[0]
const config = mergeIfValid(
// @ts-expect-error slot type mismatch - legacy API
output,
targetWidget,
targetWidget as Parameters<typeof mergeIfValid>[1],
false,
undefined,
primitiveConfig
primitiveConfig as Parameters<typeof mergeIfValid>[4]
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Type casts in mergeIfValid calls are necessary but fragile.

The casts to Parameters<typeof mergeIfValid>[n] are needed due to the dynamic nature of the widget configuration. Consider adding a brief comment explaining why these casts are necessary to aid future maintainers.

🤖 Prompt for AI Agents
In @src/extensions/core/groupNode.ts around lines 669 - 678, The mergeIfValid
call uses brittle casts (Parameters<typeof mergeIfValid>[n]) for output,
targetWidget and primitiveConfig because widget config types are dynamic; add a
concise inline comment above the const output and the mergeIfValid invocation
explaining why the casts are required (e.g., dynamic runtime widget shape,
compiler cannot infer unions) and mark them as intentional to avoid accidental
cleanup by future maintainers, keeping the casts but clarifying their necessity
and linking to the mergeIfValid function name for context.

Comment on lines 40 to 45
setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener(
'loadedmetadata',
() => resolveVideo(video),
false
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Potential double-resolution of promise.

Both the timeout (line 40) and the loadedmetadata event listener (lines 41-45) call resolveVideo(video). While calling resolve multiple times on a Promise is harmless (subsequent calls are ignored), this could be cleaner with a flag or by removing the listener after resolution.

Optional: Prevent redundant resolution
+          let resolved = false
           setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
           video.addEventListener(
             'loadedmetadata',
-            () => resolveVideo(video),
+            () => {
+              if (!resolved) {
+                resolved = true
+                resolveVideo(video)
+              }
+            },
             false
           )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener(
'loadedmetadata',
() => resolveVideo(video),
false
)
let resolved = false
setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener(
'loadedmetadata',
() => {
if (!resolved) {
resolved = true
resolveVideo(video)
}
},
false
)
🤖 Prompt for AI Agents
In @src/extensions/core/webcamCapture.ts around lines 40 - 45, The promise in
resolveVideo is being resolved both by a fallback setTimeout and the
'loadedmetadata' listener, so update the logic to avoid redundant resolution:
when attaching the 'loadedmetadata' handler for the video (the listener that
calls resolveVideo(video)), use a one-time listener (e.g., pass the once option)
or explicitly remove the listener after it fires, and make the timeout handler
clear the timeout if the event already fired (or clear the timeout inside the
event handler) so only one path calls resolveVideo; reference the existing
video.addEventListener('loadedmetadata', ...) and the setTimeout(...) fallback
when making this change.

Comment on lines 705 to 718
const ctorA = A.constructor
const ctorB = B.constructor
const priorityA =
('priority' in ctorA && typeof ctorA.priority === 'number'
? ctorA.priority
: 0) ??
('priority' in A && typeof A.priority === 'number' ? A.priority : 0)
const priorityB =
('priority' in ctorB && typeof ctorB.priority === 'number'
? ctorB.priority
: 0) ??
('priority' in B && typeof B.priority === 'number' ? B.priority : 0)
// if same priority, sort by order

return Ap == Bp ? A.order - B.order : Ap - Bp
return priorityA == priorityB ? A.order - B.order : priorityA - priorityB
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Priority extraction logic has a fallback precedence issue.

The nullish coalescing (??) will never reach the fallback because the 'in' check with a ternary always returns a number (0 as default). The ?? is effectively dead code.

Suggested fix
-      const priorityA =
-        ('priority' in ctorA && typeof ctorA.priority === 'number'
-          ? ctorA.priority
-          : 0) ??
-        ('priority' in A && typeof A.priority === 'number' ? A.priority : 0)
-      const priorityB =
-        ('priority' in ctorB && typeof ctorB.priority === 'number'
-          ? ctorB.priority
-          : 0) ??
-        ('priority' in B && typeof B.priority === 'number' ? B.priority : 0)
+      const priorityA =
+        'priority' in ctorA && typeof ctorA.priority === 'number'
+          ? ctorA.priority
+          : 'priority' in A && typeof A.priority === 'number'
+            ? A.priority
+            : 0
+      const priorityB =
+        'priority' in ctorB && typeof ctorB.priority === 'number'
+          ? ctorB.priority
+          : 'priority' in B && typeof B.priority === 'number'
+            ? B.priority
+            : 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ctorA = A.constructor
const ctorB = B.constructor
const priorityA =
('priority' in ctorA && typeof ctorA.priority === 'number'
? ctorA.priority
: 0) ??
('priority' in A && typeof A.priority === 'number' ? A.priority : 0)
const priorityB =
('priority' in ctorB && typeof ctorB.priority === 'number'
? ctorB.priority
: 0) ??
('priority' in B && typeof B.priority === 'number' ? B.priority : 0)
// if same priority, sort by order
return Ap == Bp ? A.order - B.order : Ap - Bp
return priorityA == priorityB ? A.order - B.order : priorityA - priorityB
const ctorA = A.constructor
const ctorB = B.constructor
const priorityA =
'priority' in ctorA && typeof ctorA.priority === 'number'
? ctorA.priority
: 'priority' in A && typeof A.priority === 'number'
? A.priority
: 0
const priorityB =
'priority' in ctorB && typeof ctorB.priority === 'number'
? ctorB.priority
: 'priority' in B && typeof B.priority === 'number'
? B.priority
: 0
// if same priority, sort by order
return priorityA == priorityB ? A.order - B.order : priorityA - priorityB
🤖 Prompt for AI Agents
In @src/lib/litegraph/src/LGraph.ts around lines 705 - 718, The priority
extraction uses '??' but the preceding ternary always yields a number (0), so
the fallback is never reached; update the logic in computing priorityA and
priorityB (variables ctorA, ctorB, A.priority, B.priority) to nest the ternaries
or otherwise chain checks so that if the constructor has a numeric priority use
it, else if the instance has a numeric priority use that, else use 0 (i.e.,
remove the '??' and make the ternary return the instance priority or 0 as the
final fallback).

Comment on lines +165 to +172
interface IPanel extends Element, ICloseable {
node?: LGraphNode
graph?: LGraph
}

function isPanel(el: Element): el is IPanel {
return 'close' in el && typeof el.close === 'function'
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

checkPanels() will close panels immediately unless panel.graph is set.
At Line 8040, panel.graph != this.graph will be true for panels created via createPanel() unless you assign panel.graph somewhere (currently showShowNodePanel() sets panel.node but not panel.graph). This looks like a real runtime regression (panel instantly closes on the next checkPanels() call).

Proposed fix
-interface IPanel extends Element, ICloseable {
+interface IPanel extends Element, ICloseable {
   node?: LGraphNode
-  graph?: LGraph
+  graph?: LGraph | Subgraph
 }

 function isPanel(el: Element): el is IPanel {
   return 'close' in el && typeof el.close === 'function'
 }
   showShowNodePanel(node: LGraphNode): void {
     this.SELECTED_NODE = node
     this.closePanels()
     const ref_window = this.getCanvasWindow()
     const panel = this.createPanel(node.title || '', {
       closable: true,
       window: ref_window,
       onOpen: () => {
         this.NODEPANEL_IS_OPEN = true
       },
       onClose: () => {
         this.NODEPANEL_IS_OPEN = false
         this.node_panel = null
       }
     })
     this.node_panel = panel
     panel.id = 'node-panel'
     panel.node = node
+    panel.graph = this.graph ?? undefined
     panel.classList.add('settings')

Also applies to: 8031-8042

🤖 Prompt for AI Agents
In @src/lib/litegraph/src/LGraphCanvas.ts around lines 165 - 172, checkPanels()
is closing panels because newly created panels lack panel.graph so the
comparison panel.graph != this.graph evaluates true; fix by ensuring any panel
created for this canvas has its graph set—assign panel.graph = this.graph when
creating panels (in createPanel()) and/or when showing node panels (e.g., in
showShowNodePanel() after creating the panel set panel.graph = this.graph) so
checkPanels() recognizes them as belonging to this graph.

Comment on lines 3682 to 3684
const targetEl = e.target
if (targetEl instanceof HTMLInputElement) return

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keyboard handling ignores <textarea> / contenteditable, causing shortcut leakage.
Line 3683 returns early only for HTMLInputElement. This means space/esc/copy/etc can still be handled by the canvas while focus is in a textarea (e.g., prompt multiline) or a contenteditable field (e.g., panel inline edits), which is a UX regression risk.

Proposed fix
-    const targetEl = e.target
-    if (targetEl instanceof HTMLInputElement) return
+    const targetEl = e.target
+    if (
+      targetEl instanceof HTMLInputElement ||
+      targetEl instanceof HTMLTextAreaElement ||
+      (targetEl instanceof HTMLElement && targetEl.isContentEditable) ||
+      targetEl instanceof HTMLSelectElement
+    )
+      return

Also applies to: 3720-3732

🤖 Prompt for AI Agents
In @src/lib/litegraph/src/LGraphCanvas.ts around lines 3682 - 3684, The keyboard
event early-return currently only skips when targetEl instanceof
HTMLInputElement, which lets events leak when focus is inside a textarea or
contenteditable region; update the check in the keyboard handler(s) to also
return early when targetEl is an HTMLTextAreaElement or when targetEl (or any of
its ancestors) has isContentEditable === true — i.e., replace/extend the
instanceof test on targetEl with a guard that checks HTMLInputElement OR
HTMLTextAreaElement OR walks up targetEl.parentNode to detect isContentEditable,
and apply the same fix to the other handler block referenced around lines
3720-3732 so both keyboard handling sites use the same focus-sensitive guard.

@DrJKL DrJKL closed this Jan 20, 2026
@DrJKL DrJKL deleted the drjkl/lowered-expectations branch January 28, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants