The frontend called checkMajoorVersion() unconditionally on startup, and the backend /mjr/am/releases endpoint made outbound requests to api.github.com to power it. This violates the no-internet-requests policy for core ComfyUI code, even for update checks framed as diagnostic or opt-out. Removes the endpoint, the frontend check, the now-dead version-update badge dot in HeaderSection.vue, and associated tests/docs.
625 lines
23 KiB
TypeScript
625 lines
23 KiB
TypeScript
/**
|
|
* 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 { 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<import("./features/viewer/floatingViewerManager.js")> | 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<string, any> = {}) {
|
|
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);
|
|
|
|
// 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"),
|
|
);
|
|
}
|