--- title: "Context Menu Migration Guide" --- This guide helps you migrate from the deprecated monkey-patching approach to the new context menu extension API. The old approach of monkey-patching `LGraphCanvas.prototype.getCanvasMenuOptions` and `nodeType.prototype.getExtraMenuOptions` is deprecated: If you see deprecation warnings in your browser console, your extension is using the old API and should be migrated. ## Migrating Canvas Menus ### Old Approach (Deprecated) The old approach modified the prototype during extension setup: ```javascript import { app } from "../../scripts/app.js" app.registerExtension({ name: "MyExtension", async setup() { // ❌ OLD: Monkey-patching the prototype const original = LGraphCanvas.prototype.getCanvasMenuOptions LGraphCanvas.prototype.getCanvasMenuOptions = function() { const options = original.apply(this, arguments) options.push(null) // separator options.push({ content: "My Custom Action", callback: () => { console.log("Action triggered") } }) return options } } }) ``` ### New Approach (Recommended) The new approach uses a dedicated extension hook: ```javascript import { app } from "../../scripts/app.js" app.registerExtension({ name: "MyExtension", // ✅ NEW: Use the getCanvasMenuItems hook getCanvasMenuItems(canvas) { return [ null, // separator { content: "My Custom Action", callback: () => { console.log("Action triggered") } } ] } }) ``` ### Key Differences | Old Approach | New Approach | |--------------|-------------| | Modified in `setup()` | Uses `getCanvasMenuItems()` hook | | Wraps existing function | Returns menu items directly | | Modifies `options` array | Returns new array | | Canvas accessed via `this` | Canvas passed as parameter | ## Migrating Node Menus ### Old Approach (Deprecated) The old approach modified the node type prototype: ```javascript import { app } from "../../scripts/app.js" app.registerExtension({ name: "MyExtension", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeType.comfyClass === "KSampler") { // ❌ OLD: Monkey-patching the node prototype const original = nodeType.prototype.getExtraMenuOptions nodeType.prototype.getExtraMenuOptions = function(canvas, options) { original?.apply(this, arguments) options.push({ content: "Randomize Seed", callback: () => { const seedWidget = this.widgets.find(w => w.name === "seed") if (seedWidget) { seedWidget.value = Math.floor(Math.random() * 1000000) } } }) } } } }) ``` ### New Approach (Recommended) The new approach uses a dedicated extension hook: ```javascript import { app } from "../../scripts/app.js" app.registerExtension({ name: "MyExtension", // ✅ NEW: Use the getNodeMenuItems hook getNodeMenuItems(node) { const items = [] // Add items only for specific node types if (node.comfyClass === "KSampler") { items.push({ content: "Randomize Seed", callback: () => { const seedWidget = node.widgets.find(w => w.name === "seed") if (seedWidget) { seedWidget.value = Math.floor(Math.random() * 1000000) } } }) } return items } }) ``` ### Key Differences | Old Approach | New Approach | |--------------|-------------| | Modified in `beforeRegisterNodeDef()` | Uses `getNodeMenuItems()` hook | | Type-specific via `if` check | Type-specific via `if` check in hook | | Modifies `options` array | Returns new array | | Node accessed via `this` | Node passed as parameter | ## Common Patterns ### Conditional Menu Items Both approaches support conditional items, but the new API is cleaner: ```javascript // ✅ NEW: Clean conditional logic getCanvasMenuItems(canvas) { const items = [] if (canvas.selectedItems.size > 0) { items.push({ content: `Process ${canvas.selectedItems.size} Selected Nodes`, callback: () => { // Process nodes } }) } return items } ``` ### Adding Separators Separators are added the same way in both approaches: ```javascript getCanvasMenuItems(canvas) { return [ null, // Separator (horizontal line) { content: "My Action", callback: () => {} } ] } ``` ### Creating Submenus The recommended way to create submenus is using the declarative `submenu` property: ```javascript getNodeMenuItems(node) { return [ { content: "Advanced Options", submenu: { options: [ { content: "Option 1", callback: () => {} }, { content: "Option 2", callback: () => {} } ] } } ] } ``` This declarative approach is cleaner and matches the patterns used throughout the ComfyUI codebase. While a callback-based approach with `has_submenu: true` and `new LiteGraph.ContextMenu()` is also supported, the declarative `submenu` property is preferred for better maintainability. ### Accessing State ```javascript // ✅ NEW: State access is clearer getCanvasMenuItems(canvas) { // Access canvas properties const selectedCount = canvas.selectedItems.size const graphMousePos = canvas.graph_mouse return [/* menu items */] } getNodeMenuItems(node) { // Access node properties const nodeType = node.comfyClass const isDisabled = node.mode === 2 const widgets = node.widgets return [/* menu items */] } ``` ## Troubleshooting ### How to Identify Old API Usage Look for these patterns in your code: ```javascript // ❌ Signs of old API: LGraphCanvas.prototype.getCanvasMenuOptions = function() { /* ... */ } nodeType.prototype.getExtraMenuOptions = function() { /* ... */ } ``` ### Understanding Deprecation Warnings If you see this warning in the console: ``` [DEPRECATED] Monkey-patching getCanvasMenuOptions is deprecated. (Extension: "MyExtension") Please use the new context menu API instead. See: https://docs.comfy.org/custom-nodes/js/context-menu-migration ``` Your extension is using the old approach and should be migrated. ### Verifying Migration Success After migration: 1. Remove all prototype modifications from `setup()` and `beforeRegisterNodeDef()` 2. Add `getCanvasMenuItems()` and/or `getNodeMenuItems()` hooks 3. Test that your menu items still appear correctly 4. Verify no deprecation warnings appear in the console ### Complete Migration Example **Before:** ```javascript app.registerExtension({ name: "MyExtension", async setup() { const original = LGraphCanvas.prototype.getCanvasMenuOptions LGraphCanvas.prototype.getCanvasMenuOptions = function() { const options = original.apply(this, arguments) options.push({ content: "Action", callback: () => {} }) return options } }, async beforeRegisterNodeDef(nodeType) { if (nodeType.comfyClass === "KSampler") { const original = nodeType.prototype.getExtraMenuOptions nodeType.prototype.getExtraMenuOptions = function(_, options) { original?.apply(this, arguments) options.push({ content: "Node Action", callback: () => {} }) } } } }) ``` **After:** ```javascript app.registerExtension({ name: "MyExtension", getCanvasMenuItems(canvas) { return [ { content: "Action", callback: () => {} } ] }, getNodeMenuItems(node) { if (node.comfyClass === "KSampler") { return [ { content: "Node Action", callback: () => {} } ] } return [] } }) ``` ## Additional Resources - [Annotated Examples](./javascript_examples) - More examples using the new API - [Extension Hooks](./javascript_hooks) - Complete list of available extension hooks - [Commands and Keybindings](./javascript_commands_keybindings) - Add keyboard shortcuts to your menu actions