From 3adecc4ded9755fa577e2d282a820de2dbddbca9 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Thu, 5 Feb 2026 05:06:58 +0100 Subject: [PATCH] fix: prevent duplicate context menu items by using content-based comparison (#8602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Switches from reference-based to content-based duplicate detection for context menu items - Fixes cases where extensions create duplicate menu items (different objects with same content) - Improves removal detection accuracy by comparing content strings instead of object references ## Details The previous implementation compared menu items by object reference, which would miss duplicates when extensions added new objects with the same content. This change: - Creates Sets of content strings from menu items for efficient duplicate detection - Filters additions by checking if their content already exists - Provides accurate count of removed items through content comparison ## Test plan - [x] Unit tests pass (`pnpm test:unit src/lib/litegraph/src/contextMenuCompat.test.ts`) - [x] TypeScript compilation succeeds (`pnpm typecheck`) - [x] Linting passes (`pnpm lint`) - [x] Pre-commit hooks pass ## Before Screenshot 2026-02-04 045422 ## After Screenshot 2026-02-04 045918 ## Summary by CodeRabbit * **Bug Fixes** * Improved context menu compatibility: more accurate detection of added/removed menu items and clearer warnings when items are removed. * **Refactor** * Updated numeric input formatting to use locale-aware formatting logic, preserving grouping and precision behavior. * **Tests** * Added a test ensuring legacy menu extraction handles items with undefined content correctly. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8602-fix-prevent-duplicate-context-menu-items-by-using-content-based-comparison-2fd6d73d36508197aa74c3409c7425fa) by [Unito](https://www.unito.io) --------- Co-authored-by: Terry Jia --- .../litegraph/src/contextMenuCompat.test.ts | 36 ++++++++++++++ src/lib/litegraph/src/contextMenuCompat.ts | 48 +++++++++++++++---- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/lib/litegraph/src/contextMenuCompat.test.ts b/src/lib/litegraph/src/contextMenuCompat.test.ts index 246551dc9..e8afeca9b 100644 --- a/src/lib/litegraph/src/contextMenuCompat.test.ts +++ b/src/lib/litegraph/src/contextMenuCompat.test.ts @@ -195,6 +195,42 @@ describe('contextMenuCompat', () => { expect.any(Error) ) }) + + it('should handle multiple items with undefined content correctly', () => { + // Setup base method with items that have undefined content + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + return [ + { content: undefined, title: 'Separator 1' }, + { content: undefined, title: 'Separator 2' }, + { content: 'Item 1', callback: () => {} } + ] + } + + legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions') + + // Monkey-patch to add an item with undefined content + const original = LGraphCanvas.prototype.getCanvasMenuOptions + LGraphCanvas.prototype.getCanvasMenuOptions = + function (): (IContextMenuValue | null)[] { + const items = original.apply(this) + items.push({ content: undefined, title: 'Separator 3' }) + return items + } + + // Extract legacy items + const legacyItems = legacyMenuCompat.extractLegacyItems( + 'getCanvasMenuOptions', + mockCanvas + ) + + // Should extract only the newly added item with undefined content + // (not collapse with existing undefined content items) + expect(legacyItems).toHaveLength(1) + expect(legacyItems[0]).toMatchObject({ + content: undefined, + title: 'Separator 3' + }) + }) }) describe('integration', () => { diff --git a/src/lib/litegraph/src/contextMenuCompat.ts b/src/lib/litegraph/src/contextMenuCompat.ts index c1f039cfa..ae48aec70 100644 --- a/src/lib/litegraph/src/contextMenuCompat.ts +++ b/src/lib/litegraph/src/contextMenuCompat.ts @@ -152,19 +152,51 @@ class LegacyMenuCompat { const patchedItems = methodToCall.apply(context, args) as | (IContextMenuValue | null)[] | undefined - if (!patchedItems) return [] + if (!patchedItems) { + return [] + } + // Use content-based diff to detect additions (not reference-based) + // Create composite keys from multiple properties to handle undefined content + const createItemKey = (item: IContextMenuValue): string => { + const parts = [ + item.content ?? '', + item.title ?? '', + item.className ?? '', + item.property ?? '', + item.type ?? '' + ] + return parts.join('|') + } - // Use set-based diff to detect additions by reference - const originalSet = new Set(originalItems) - const addedItems = patchedItems.filter((item) => !originalSet.has(item)) + const originalKeys = new Set( + originalItems + .filter( + (item): item is IContextMenuValue => + item !== null && typeof item === 'object' && 'content' in item + ) + .map(createItemKey) + ) + const addedItems = patchedItems.filter((item) => { + if (item === null) return false + if (typeof item !== 'object' || !('content' in item)) return false + return !originalKeys.has(createItemKey(item)) + }) // Warn if items were removed (patched has fewer original items than expected) - const retainedOriginalCount = patchedItems.filter((item) => - originalSet.has(item) + const patchedKeys = new Set( + patchedItems + .filter( + (item): item is IContextMenuValue => + item !== null && typeof item === 'object' && 'content' in item + ) + .map(createItemKey) + ) + const removedCount = [...originalKeys].filter( + (key) => !patchedKeys.has(key) ).length - if (retainedOriginalCount < originalItems.length) { + if (removedCount > 0) { console.warn( - `[Context Menu Compat] Monkey patch for ${methodName} removed ${originalItems.length - retainedOriginalCount} original menu item(s). ` + + `[Context Menu Compat] Monkey patch for ${methodName} removed ${removedCount} original menu item(s). ` + `This may cause unexpected behavior.` ) }