/** * Entry Point - Majoor Assets Manager * * Registers the ComfyUI extension and wires up the Vue-based UI. * * Vue migration summary * --------------------- * - Sidebar and bottom-panel tabs mount isolated Vue 3 + Pinia applications * (see ui/vue/App.vue and ui/vue/GeneratedFeedApp.vue). * - Keep-alive is handled by mountKeepAlive() in createVueApp.js - no more * _mjrKeepAliveMounted DOM flags. * - Commands are declared on the extension object (declarative API) so ComfyUI * surfaces them in the palette and shortcut editor automatically. * - Toast notifications route through app.extensionManager.toast (ComfyUI's * PrimeVue toast) with a DOM fallback - no changes needed in toast.js as it * already prefers the native API. */ // Side-effect import: starts the very first /list request immediately when this // module is parsed (well before ComfyUI invokes our `setup()` step 8b). Keep it // at the very top - its tiny dep set (api/client + endpoints + config) ensures // nothing heavy runs ahead of it. Result is consumed by useGridLoader.fetchPage. import "./features/runtime/earlyFetch.js"; // Side-effect: registers the "Send to Asset Manager" right-click menu on // SaveImage / VHS_VideoCombine etc. Isolated from the Vue runtime so a // failure here cannot break the asset grid. import "./integration/comfy_send_to_am.js"; import "./integration/majoor_save_placeholders.js"; import { testAPI, triggerStartupScan } from "./app/bootstrap.js"; import { checkMajoorVersion } from "./app/versionCheck.js"; import { ensureStyleLoaded } from "./app/style.js"; import { buildMajoorSettings, registerMajoorSettings } from "./app/settings.js"; import { getComfyApi, setComfyApi, setComfyApp, waitForComfyApi, waitForComfyApp, } from "./app/comfyApiBridge.js"; import { EVENTS } from "./app/events.js"; import { ensureFloatingViewerProgressTracking } from "./features/viewer/floatingViewerProgress.js"; import { NODE_STREAM_FEATURE_ENABLED, NODE_STREAM_REACTIVATION_DOC, } from "./features/viewer/nodeStream/nodeStreamFeatureFlag.js"; import { loadAssets, upsertAsset, upsertAssetNow, removeAssetsFromGrid, } from "./features/grid/gridApi.js"; import { getActiveGridContainer } from "./features/panel/panelRuntimeRefs.js"; import { pushGeneratedAsset, refreshGeneratedFeedHosts, } from "./features/bottomPanel/feed/feedHost.js"; import { createExecutionRuntimeController } from "./features/stacks/executionRuntimeController.js"; import { extractOutputFiles } from "./utils/extractOutputFiles.js"; import { post } from "./api/client.js"; import { ENDPOINTS } from "./api/endpoints.js"; import { comfyToast } from "./app/toast.js"; import { t } from "./app/i18n.js"; import { APP_CONFIG } from "./app/config.js"; import { getRuntimeEnrichmentState, setRuntimeEnrichmentState, } from "./stores/runtimeEnrichmentState.js"; import { reportError, mjrDbg } from "./utils/logging.js"; // @ts-ignore - external ComfyUI module import { app } from "../../scripts/app.js"; import { registerRealtimeListeners } from "./features/runtime/registerRealtimeListeners.js"; import { exposeDebugApis } from "./features/runtime/entryDebugApi.js"; import { installBenignConsoleNoiseFilter } from "./features/runtime/benignConsoleNoise.js"; import { registerGenInfoOverridePicker } from "./features/geninfo/genInfoOverridePicker.js"; import { buildAboutPageBadges, buildBottomPanelTabs, buildNativeCommands, buildNativeKeybindings, buildNativeMenuCommands, getMajoorNodeMenuItems, getMajoorCanvasMenuItems, getMajoorSelectionToolboxCommands, mountGlobalRuntime, prewarmAssetsSidebar, registerAssetsSidebar, registerNativeCommands, registerNativeKeybindings, startEarlyFetch, teardownTopBarMfvButton, teardownGeneratedFeed, teardownAssetsSidebar, teardownGlobalRuntime, } from "./features/runtime/entryUiRegistration.js"; import { ENTRY_RUNTIME_KEY, cleanupEntryRuntime, installEntryRuntimeController, registerCleanableListener, removeApiHandlers, removeRuntimeWindowHandlers, } from "./features/runtime/entryRuntimeLifecycle.js"; // -- lazy module handles ------------------------------------------------------- /** @type {import("./features/viewer/LiveStreamTracker.js") | null} */ let liveStreamModule = null; /** @type {import("./features/viewer/nodeStream/NodeStreamController.js") | null} */ let nodeStreamModule = null; /** @type {Promise | null} */ let floatingViewerManagerModulePromise = null; function loadFloatingViewerManagerModule() { if (!floatingViewerManagerModulePromise) { floatingViewerManagerModulePromise = import("./features/viewer/floatingViewerManager.js"); } return floatingViewerManagerModulePromise; } function teardownLoadedFloatingViewerManager() { if (!floatingViewerManagerModulePromise) return; void floatingViewerManagerModulePromise .then((mod) => mod?.teardownFloatingViewerManager?.()) .catch((e) => console.debug?.("[Majoor] MFV teardown skipped", e)); } // -- constants ----------------------------------------------------------------- const SIDEBAR_TAB_ID = "majoor-assets"; const EXECUTION_RUNTIME_KEY = "__MJR_EXECUTION_RUNTIME__"; const EXTENSION_NAME = "Majoor.AssetsManager"; let _lastExecutionBackendSync = { active: null, promptId: "" }; let _deferredGridReloadTimer = null; const NODE_STREAM_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const NODE_STREAM_HASH_RE = /^[0-9a-f]{20,}$/i; function _nodeStreamFirstText(...values) { for (const value of values) { const text = String(value || "").trim(); if (text) return text; } return ""; } function _isOpaqueNodeStreamType(type) { const text = String(type || "").trim(); return NODE_STREAM_UUID_RE.test(text) || NODE_STREAM_HASH_RE.test(text); } function _getNodeStreamTitle(node) { return _nodeStreamFirstText( node?.title, node?.properties?.title, node?.properties?.name, node?.properties?.label, node?.name, ); } function _getNodeStreamGraphNodes(graph) { if (!graph || typeof graph !== "object") return []; if (Array.isArray(graph.nodes)) return graph.nodes.filter(Boolean); if (Array.isArray(graph._nodes)) return graph._nodes.filter(Boolean); const byId = graph._nodes_by_id ?? graph.nodes_by_id ?? null; if (byId instanceof Map) return Array.from(byId.values()).filter(Boolean); if (byId && typeof byId === "object") return Object.values(byId).filter(Boolean); return []; } function _nodeStreamHasGraphNodes(graph) { return _getNodeStreamGraphNodes(graph).length > 0; } function _isNodeStreamSubgraph(node) { if (!node || typeof node !== "object") return false; const candidates = [ node.subgraph, node._subgraph, node.subgraph?.graph, node.subgraph?.lgraph, node.properties?.subgraph, node.subgraph_instance, node.subgraph_instance?.graph, node.inner_graph, node.subgraph_graph, ]; if (candidates.some(_nodeStreamHasGraphNodes)) return true; if (Array.isArray(node.nodes) && node.nodes.length > 0 && node.nodes !== node.graph?.nodes) { return true; } return _isOpaqueNodeStreamType(node.type) && Boolean(_getNodeStreamTitle(node)); } function _getNodeStreamClassLabel(node, fallbackClassType) { const type = String(node?.type || fallbackClassType || "").trim(); const title = _getNodeStreamTitle(node); if (_isOpaqueNodeStreamType(type)) return title || "Subgraph"; return type || title || "Node"; } // -- execution runtime helpers ------------------------------------------------- function ensureExecutionRuntime() { try { if (typeof window === "undefined") { return { active_prompt_id: null, queue_remaining: null, progress_node: null, progress_value: null, progress_max: null, cached_nodes: [], }; } if (!window[EXECUTION_RUNTIME_KEY] || typeof window[EXECUTION_RUNTIME_KEY] !== "object") { window[EXECUTION_RUNTIME_KEY] = { active_prompt_id: null, queue_remaining: null, progress_node: null, progress_value: null, progress_max: null, cached_nodes: [], }; } return window[EXECUTION_RUNTIME_KEY]; } catch (e) { console.debug?.(e); return {}; } } function emitRuntimeStatus(partial = {}) { try { const runtime = ensureExecutionRuntime(); Object.assign(runtime, partial || {}); window.dispatchEvent(new CustomEvent(EVENTS.RUNTIME_STATUS, { detail: { ...(runtime as any) } })); } catch (e) { console.debug?.(e); } } async function syncExecutionBackendState({ active, promptId = "" }: Record = {}) { const nextActive = !!active; const nextPromptId = String(promptId || "").trim(); if ( _lastExecutionBackendSync.active === nextActive && _lastExecutionBackendSync.promptId === nextPromptId ) { return; } _lastExecutionBackendSync = { active: nextActive, promptId: nextPromptId }; try { await post(ENDPOINTS.RUNTIME_EXECUTION, { active: nextActive, prompt_id: nextPromptId || undefined, cooldown_ms: Number(APP_CONFIG.EXECUTION_IDLE_GRACE_MS) || 6000, }); } catch (e) { reportError(e, "entry.execution_state_sync"); } } function isExecutionActive() { try { return !!String((ensureExecutionRuntime() as any)?.active_prompt_id || "").trim(); } catch (e) { console.debug?.(e); return false; } } function scheduleGridReloadWhenIdle(delayMs = 1200) { try { if (_deferredGridReloadTimer) { clearTimeout(_deferredGridReloadTimer); } } catch (e) { console.debug?.(e); } _deferredGridReloadTimer = setTimeout( () => { _deferredGridReloadTimer = null; if (isExecutionActive()) { scheduleGridReloadWhenIdle(delayMs); return; } const grid = getActiveGridContainer(); if (grid) { loadAssets(grid); } }, Math.max(250, Number(delayMs) || 0), ); } // -- lazy module initialisation ------------------------------------------------- function setupLazyModules(runtimeApp) { void import("./features/viewer/LiveStreamTracker.js") .then((mod) => { liveStreamModule = mod; try { mod.initLiveStreamTracker(runtimeApp); } catch (e) { console.warn("[MJR setup] initLiveStreamTracker failed:", e); } }) .catch((e) => console.warn("[MJR setup] LiveStreamTracker load failed:", e)); if (!NODE_STREAM_FEATURE_ENABLED) { console.debug("[Majoor] Node Stream disabled by feature flag."); return; } void import("./features/viewer/nodeStream/NodeStreamController.js") .then((mod) => { nodeStreamModule = mod; try { mod.initNodeStream({ app: runtimeApp, onOutput: (fileData) => { void loadFloatingViewerManagerModule() .then((mfv) => mfv?.floatingViewerManager?.feedNodeStream?.(fileData)) .catch((e) => console.debug?.("[NodeStream] MFV output failed", e)); }, onStatus: (nodeId, classType) => { try { const graph = runtimeApp?.graph ?? runtimeApp?.canvas?.graph ?? null; const node = graph?.getNodeById?.(Number(nodeId)); const isSubgraph = _isNodeStreamSubgraph(node); const title = _getNodeStreamTitle(node); const displayClass = isSubgraph ? "Subgraph" : _getNodeStreamClassLabel(node, classType); void loadFloatingViewerManagerModule() .then((mfv) => { mfv?.floatingViewerManager?.setNodeStreamSelection?.( nodeId, displayClass, isSubgraph ? title || _getNodeStreamClassLabel(node, classType) : title, ); }) .catch((err) => console.debug?.("[NodeStream] MFV status failed", err)); } catch (err) { console.debug?.("[NodeStream] onStatus failed", err); } }, }); } catch (e) { console.warn("[MJR setup] initNodeStream failed:", e); } }) .catch((e) => console.warn("[MJR setup] NodeStream load failed:", e)); } // -- API listener setup -------------------------------------------------------- async function setupApiListeners(runtimeApp, executionRuntime) { const api = (await waitForComfyApi({ app: runtimeApp, timeoutMs: 4000 })) || getComfyApi(runtimeApp); setComfyApi(api || null); if (!api) { console.warn("Majoor API not available, real-time updates disabled"); return; } void ensureFloatingViewerProgressTracking({ api, app: runtimeApp }).catch((e) => console.debug?.("[Majoor] MFV progress tracking init failed", e), ); let runtime = null; try { runtime = typeof window !== "undefined" ? window[ENTRY_RUNTIME_KEY] || null : null; } catch (e) { console.debug?.(e); } removeApiHandlers(runtime?.api || null, reportError); if (api !== runtime?.api) removeApiHandlers(api, reportError); removeRuntimeWindowHandlers(runtime, reportError); await registerRealtimeListeners({ api, runtime, executionRuntime, appRef: app, liveStreamModule, ensureExecutionRuntime, emitRuntimeStatus, getActiveGridContainer, pushGeneratedAsset, upsertAsset, upsertAssetNow, removeAssetsFromGrid, getEnrichmentState: getRuntimeEnrichmentState, setEnrichmentState: setRuntimeEnrichmentState, comfyToast, t, reportError, registerCleanableListener, syncExecutionBackendState, }); } // -- execution runtime controller ---------------------------------------------- const executionRuntime = createExecutionRuntimeController({ post, ENDPOINTS, reportError, extractOutputFiles, ensureExecutionRuntime, emitRuntimeStatus, refreshGeneratedFeedHosts, getActiveGridContainer, }); // -- extension registration ----------------------------------------------------- // Build the extension object once so it can be re-used if `registerExtension` // throws below (see try/catch after the object literal). const majoorExtension = { name: EXTENSION_NAME, /** * Declarative settings - registered with ComfyUI's settings panel. * Replaces the imperative registerMajoorSettings() call for the settings UI. */ settings: buildMajoorSettings(app), /** * Declarative commands - ComfyUI surfaces these in the command palette * and the shortcut editor without additional imperative calls. */ commands: buildNativeCommands(app, { sidebarTabId: SIDEBAR_TAB_ID, triggerStartupScan, }), keybindings: buildNativeKeybindings(), menuCommands: buildNativeMenuCommands(), aboutPageBadges: buildAboutPageBadges(), /** * Declarative bottom-panel tabs - processed by ComfyUI's extension service. * Each tab's render/destroy callbacks mount/unmount the Vue app via * mountKeepAlive() so state survives panel collapse/expand cycles. */ bottomPanelTabs: buildBottomPanelTabs(), // -- lifecycle -------------------------------------------------------------- async setup() { // 1. Runtime lifecycle controller (handles extension cleanup on reload). installEntryRuntimeController({ cleanupEntryRuntimeFn: cleanupEntryRuntime, teardownLiveStreamTracker: (runtimeApp) => liveStreamModule?.teardownLiveStreamTracker(runtimeApp), teardownNodeStream: (runtimeApp) => nodeStreamModule?.teardownNodeStream(runtimeApp), teardownFloatingViewerManager: teardownLoadedFloatingViewerManager, teardownGeneratedFeed, teardownAssetsSidebar, teardownGlobalRuntime, teardownTopBarMfvButton, reportError, }); installBenignConsoleNoiseFilter(); // 2. Resolve the ComfyUI app reference (may take a few frames to be ready). setComfyApp(app); const runtimeApp = (await waitForComfyApp({ timeoutMs: 12000 })) || app; setComfyApp(runtimeApp); registerGenInfoOverridePicker(runtimeApp); try { if (typeof window !== "undefined") (window as any).__MJR_RUNTIME_APP__ = runtimeApp; } catch (e) { console.debug?.(e); } // 3. Baseline checks. testAPI(); ensureStyleLoaded({ enabled: true }); mountGlobalRuntime(); // 4. Global runtime - viewer + DnD stay mounted from the always-on Vue runtime. // 5. Lazy viewer modules (non-blocking). setupLazyModules(runtimeApp); // 6. Settings runtime init (applies settings to APP_CONFIG, starts watcher sync...). registerMajoorSettings(runtimeApp, () => { const grid = getActiveGridContainer(); if (!grid) return; if (APP_CONFIG.DEFER_GRID_FETCH_DURING_EXECUTION && isExecutionActive()) { scheduleGridReloadWhenIdle(); return; } loadAssets(grid); }); // 7. Fallback command registration for older ComfyUI versions that don't // process the declarative `commands` array on the extension object. registerNativeCommands(runtimeApp, { sidebarTabId: SIDEBAR_TAB_ID, triggerStartupScan, }); registerNativeKeybindings(runtimeApp); // 8. Version check (deferred so it doesn't slow startup). setTimeout(() => { void checkMajoorVersion(); }, 5000); // 9. Proactive early fetch - start the first assets page in the background // so the data is ready (or already in-flight) when the user opens the // sidebar. Fire immediately: previous 800ms timeout was wasted latency // that delayed the first paint when the user clicked the sidebar fast. // startEarlyFetch() is a no-op when ComfyUI is executing a prompt and // its TTL is wide enough to survive slow extension registration. try { startEarlyFetch(); } catch (e) { console.debug?.(e); } // 9. WebSocket / realtime listeners. void setupApiListeners(runtimeApp, executionRuntime).catch((e) => reportError(e, "entry.api_setup"), ); // 10. Register sidebar tab (Vue app mounts on first open via mountKeepAlive). if ( registerAssetsSidebar(runtimeApp, { sidebarTabId: SIDEBAR_TAB_ID, }) ) { mjrDbg("[Majoor] Sidebar tab registered (Vue)"); } else { console.warn( "Majoor Assets Manager: extensionManager.registerSidebarTab is unavailable", ); } // 10b. Prewarm the sidebar Vue app on a detached host during browser // idle so the click->cards latency drops from ~2 s (cold mount) to one // DOM frame (host re-attach). See entryUiRegistration.prewarmAssetsSidebar. try { const _ric = typeof window !== "undefined" ? window.requestIdleCallback : null; const _doPrewarm = () => { try { prewarmAssetsSidebar(); } catch (e) { console.debug?.(e); } }; if (typeof _ric === "function") { _ric(_doPrewarm, { timeout: 2500 }); } else { setTimeout(_doPrewarm, 600); } } catch (e) { console.debug?.(e); } // 11. Bottom panel tab is registered declaratively via `bottomPanelTabs`. // 12. Startup warmup: API/DB/counters first, then idle index scan. // 12. Debug API surface (development/support). exposeDebugApis({ resolveNodeStreamModule: async () => { if (!nodeStreamModule) { nodeStreamModule = await import("./features/viewer/nodeStream/NodeStreamController.js"); } return nodeStreamModule; }, }); }, // -- hooks ------------------------------------------------------------------ /** * ComfyUI v1.42+ - fires when individual node outputs are updated during * a workflow execution. Feeds the execution-stack runtime so the bottom * feed panel receives live assets. */ onNodeOutputsUpdated(nodeOutputs) { try { executionRuntime.handleNodeOutputsUpdated(nodeOutputs); } catch (e) { console.debug?.("[Majoor] onNodeOutputsUpdated error", e); } }, /** Context-menu items injected into ComfyUI node right-click menus. */ getNodeMenuItems(node) { return getMajoorNodeMenuItems(node, app, { sidebarTabId: SIDEBAR_TAB_ID }); }, /** Context-menu items injected into ComfyUI canvas right-click menus. */ getCanvasMenuItems(canvas) { void canvas; return getMajoorCanvasMenuItems(app, { sidebarTabId: SIDEBAR_TAB_ID, triggerStartupScan, }); }, /** ComfyUI v1.10.9+ selection toolbox buttons for trackable canvas nodes. */ getSelectionToolboxCommands(selectedItem) { return getMajoorSelectionToolboxCommands(selectedItem); }, }; try { app.registerExtension(majoorExtension); } catch (e) { // ComfyUI's registerExtension() throws "Extension named '...' already // registered" on a second call for the same name, and there is no // unregisterExtension API to undo the first registration. That means a // live re-import of this module (module hot-reload, or a manual // "reload extension" action without a full page refresh) throws here // instead of ComfyUI invoking setup() again on the existing // registration - so the reload-safe teardown/reinit path below would // otherwise never run. Run setup() directly so it still does. console.warn( "[Majoor] registerExtension() threw (extension already registered) - re-running setup() directly", e, ); void majoorExtension.setup().catch((setupError: unknown) => reportError(setupError, "entry.setup_after_duplicate_registration"), ); }