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
## After
## 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.`
)
}