Files

843 lines
34 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { lmUrl } from "./base_path.js";
// ============================================================================
// Setting IDs and Defaults
// ============================================================================
const TRIGGER_WORD_WHEEL_SENSITIVITY_ID = "loramanager.trigger_word_wheel_sensitivity";
const TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT = 0.02;
const AUTO_PATH_CORRECTION_SETTING_ID = "loramanager.auto_path_correction";
const AUTO_PATH_CORRECTION_DEFAULT = true;
const PROMPT_TAG_AUTOCOMPLETE_SETTING_ID = "loramanager.prompt_tag_autocomplete";
const PROMPT_TAG_AUTOCOMPLETE_DEFAULT = true;
const AUTOCOMPLETE_APPEND_COMMA_SETTING_ID = "loramanager.autocomplete_append_comma";
const AUTOCOMPLETE_APPEND_COMMA_DEFAULT = true;
const AUTOCOMPLETE_AUTO_FORMAT_SETTING_ID = "loramanager.autocomplete_auto_format";
const AUTOCOMPLETE_AUTO_FORMAT_DEFAULT = true;
const AUTOCOMPLETE_ACCEPT_KEY_SETTING_ID = "loramanager.autocomplete_accept_key";
const AUTOCOMPLETE_ACCEPT_KEY_DEFAULT = "both";
const AUTOCOMPLETE_ACCEPT_KEY_OPTION_BOTH = "Tab or Enter";
const AUTOCOMPLETE_ACCEPT_KEY_OPTION_TAB_ONLY = "Tab only";
const AUTOCOMPLETE_ACCEPT_KEY_OPTION_ENTER_ONLY = "Enter only";
const TAG_SPACE_REPLACEMENT_SETTING_ID = "loramanager.tag_space_replacement";
const TAG_SPACE_REPLACEMENT_DEFAULT = false;
const USAGE_STATISTICS_SETTING_ID = "loramanager.usage_statistics";
const USAGE_STATISTICS_DEFAULT = true;
const ANIMA_LORA_BLOCK_REMAP_SETTING_ID = "loramanager.anima_lora_block_remap";
const ANIMA_LORA_BLOCK_REMAP_DEFAULT = true;
const NEW_TAB_TEMPLATE_ID = "loramanager.new_tab_template";
const NEW_TAB_TEMPLATE_DEFAULT = "Default";
const NEW_TAB_ZOOM_LEVEL = 0.8;
const STRENGTH_STEP_SETTING_ID = "loramanager.strength_step";
const STRENGTH_STEP_DEFAULT = 0.05;
const LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID = "loramanager.lora_active_filters_autocomplete";
const LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT = false;
const OPEN_COMFYUI_SETTINGS_MESSAGE = "lm:open-comfy-settings";
const OPEN_COMFYUI_SETTINGS_QUERY = "lm_open_settings";
const COMFYUI_SETTINGS_CHANNEL = "lm:comfyui-settings";
const DISCOVER_COMFYUI_SETTINGS_MESSAGE = "lm:discover-comfyui-settings";
const COMFYUI_SETTINGS_AVAILABLE_MESSAGE = "lm:comfyui-settings-available";
// Node widget rows keep their own label preference. It is a ComfyUI setting
// because it changes how a node renders, and it is deliberately independent of
// the LoRA Manager browser's `model_name_display`: one governs cards you read
// while browsing, the other names what a graph will load. Default is the file
// name, so existing behavior does not change.
const NODE_LORA_NAME_DISPLAY_SETTING_ID = "loramanager.node_lora_name_display";
const NODE_LORA_NAME_DISPLAY_OPTION_FILE_NAME = "File Name";
const NODE_LORA_NAME_DISPLAY_OPTION_MODEL_NAME = "Model Name";
const NODE_LORA_NAME_DISPLAY_DEFAULT = "file_name";
// ============================================================================
// Helper Functions
// ============================================================================
let workflowOptions = [NEW_TAB_TEMPLATE_DEFAULT];
let workflowOptionsFull = [{ value: "Default", label: "Default (Blank)", path: null }];
let workflowOptionsLoaded = false;
const loadWorkflowOptions = async () => {
if (workflowOptionsLoaded) {
return;
}
try {
const response = await fetch(lmUrl("/api/lm/example-workflows"));
const data = await response.json();
if (data.success && data.workflows) {
workflowOptionsFull = data.workflows;
workflowOptions = data.workflows.map((w) => w.label);
workflowOptionsLoaded = true;
}
} catch (error) {
console.warn("LoRA Manager: Failed to fetch workflow options", error);
}
};
const getWorkflowOptions = () => {
// Function may be called with or without parameters
// Return the current workflow options array
return workflowOptions;
};
const loadTemplateWorkflow = async (templateName) => {
if (!templateName || templateName === NEW_TAB_TEMPLATE_DEFAULT) {
return null;
}
try {
const workflow = workflowOptionsFull.find((w) => w.label === templateName);
if (workflow && workflow.value) {
const workflowResponse = await fetch(
lmUrl(`/api/lm/example-workflows/${encodeURIComponent(workflow.value)}`)
);
const workflowData = await workflowResponse.json();
if (workflowData.success && workflowData.workflow) {
return workflowData.workflow;
}
}
} catch (error) {
console.error("LoRA Manager: Failed to load template workflow", error);
}
return null;
};
const getWheelSensitivity = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default wheel sensitivity.");
settingsUnavailableLogged = true;
}
return TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT;
}
try {
const value = settingManager.get(TRIGGER_WORD_WHEEL_SENSITIVITY_ID);
return value ?? TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read wheel sensitivity setting, using default.", error);
settingsUnavailableLogged = true;
}
return TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT;
}
};
})();
const getAutoPathCorrectionPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, defaulting auto path correction to enabled.");
settingsUnavailableLogged = true;
}
return AUTO_PATH_CORRECTION_DEFAULT;
}
try {
const value = settingManager.get(AUTO_PATH_CORRECTION_SETTING_ID);
return value ?? AUTO_PATH_CORRECTION_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read auto path correction setting, defaulting to enabled.", error);
settingsUnavailableLogged = true;
}
return AUTO_PATH_CORRECTION_DEFAULT;
}
};
})();
const getPromptTagAutocompletePreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default tag autocomplete setting.");
settingsUnavailableLogged = true;
}
return PROMPT_TAG_AUTOCOMPLETE_DEFAULT;
}
try {
const value = settingManager.get(PROMPT_TAG_AUTOCOMPLETE_SETTING_ID);
return value ?? PROMPT_TAG_AUTOCOMPLETE_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read tag autocomplete setting, using default.", error);
settingsUnavailableLogged = true;
}
return PROMPT_TAG_AUTOCOMPLETE_DEFAULT;
}
};
})();
/**
* Persist a LoRA Manager setting through ComfyUI's setting API.
* Returns true when the setting was written successfully.
*/
const setLoraManagerSettingValue = async (settingId, value) => {
const settingManager = app?.extensionManager?.setting;
if (settingManager && typeof settingManager.set === "function") {
await settingManager.set(settingId, value);
return true;
}
const setting = app?.ui?.settings?.settingsById?.[settingId];
if (setting) {
app.ui.settings.setSettingValue(settingId, value);
return true;
}
return false;
};
// The Anima remap toggle lives in ComfyUI's settings (it controls a node's
// behavior), but the value the Python loader reads is stored in LoRA Manager's
// settings API. Keep the two in sync: pull the backend value on startup, push
// user changes back to the backend, and ignore the change event fired by the
// initial sync.
let animaLoraBlockRemapSynced = false;
const persistAnimaLoraBlockRemap = async (value) => {
try {
const response = await fetch(lmUrl("/api/lm/settings"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ anima_lora_block_remap: value }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.warn("LoRA Manager: failed to persist Anima LoRA block remap setting", error);
}
};
const syncAnimaLoraBlockRemapFromBackend = async () => {
try {
const response = await fetch(lmUrl("/api/lm/settings"));
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const value = data?.settings?.anima_lora_block_remap !== false;
await setLoraManagerSettingValue(ANIMA_LORA_BLOCK_REMAP_SETTING_ID, value);
} catch (error) {
console.warn("LoRA Manager: failed to sync Anima LoRA block remap setting", error);
} finally {
animaLoraBlockRemapSynced = true;
}
};
const getAutocompleteAppendCommaPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default append comma setting.");
settingsUnavailableLogged = true;
}
return AUTOCOMPLETE_APPEND_COMMA_DEFAULT;
}
try {
const value = settingManager.get(AUTOCOMPLETE_APPEND_COMMA_SETTING_ID);
return value ?? AUTOCOMPLETE_APPEND_COMMA_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read append comma setting, using default.", error);
settingsUnavailableLogged = true;
}
return AUTOCOMPLETE_APPEND_COMMA_DEFAULT;
}
};
})();
const getAutocompleteAutoFormatPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default autocomplete auto format setting.");
settingsUnavailableLogged = true;
}
return AUTOCOMPLETE_AUTO_FORMAT_DEFAULT;
}
try {
const value = settingManager.get(AUTOCOMPLETE_AUTO_FORMAT_SETTING_ID);
return value ?? AUTOCOMPLETE_AUTO_FORMAT_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read autocomplete auto format setting, using default.", error);
settingsUnavailableLogged = true;
}
return AUTOCOMPLETE_AUTO_FORMAT_DEFAULT;
}
};
})();
const getAutocompleteAcceptKeyPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default autocomplete accept key setting.");
settingsUnavailableLogged = true;
}
return AUTOCOMPLETE_ACCEPT_KEY_DEFAULT;
}
try {
const value = settingManager.get(AUTOCOMPLETE_ACCEPT_KEY_SETTING_ID);
if (value === AUTOCOMPLETE_ACCEPT_KEY_OPTION_TAB_ONLY) {
return "tab_only";
}
if (value === AUTOCOMPLETE_ACCEPT_KEY_OPTION_ENTER_ONLY) {
return "enter_only";
}
if (value === AUTOCOMPLETE_ACCEPT_KEY_OPTION_BOTH || value == null) {
return AUTOCOMPLETE_ACCEPT_KEY_DEFAULT;
}
return value;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read autocomplete accept key setting, using default.", error);
settingsUnavailableLogged = true;
}
return AUTOCOMPLETE_ACCEPT_KEY_DEFAULT;
}
};
})();
const getTagSpaceReplacementPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default tag space replacement setting.");
settingsUnavailableLogged = true;
}
return TAG_SPACE_REPLACEMENT_DEFAULT;
}
try {
const value = settingManager.get(TAG_SPACE_REPLACEMENT_SETTING_ID);
return value ?? TAG_SPACE_REPLACEMENT_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read tag space replacement setting, using default.", error);
settingsUnavailableLogged = true;
}
return TAG_SPACE_REPLACEMENT_DEFAULT;
}
};
})();
const getUsageStatisticsPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default usage statistics setting.");
settingsUnavailableLogged = true;
}
return USAGE_STATISTICS_DEFAULT;
}
try {
const value = settingManager.get(USAGE_STATISTICS_SETTING_ID);
return value ?? USAGE_STATISTICS_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read usage statistics setting, using default.", error);
settingsUnavailableLogged = true;
}
return USAGE_STATISTICS_DEFAULT;
}
};
})();
const getNewTabTemplatePreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default new tab template.");
settingsUnavailableLogged = true;
}
return NEW_TAB_TEMPLATE_DEFAULT;
}
try {
const value = settingManager.get(NEW_TAB_TEMPLATE_ID);
return value ?? NEW_TAB_TEMPLATE_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read new tab template setting, using default.", error);
settingsUnavailableLogged = true;
}
return NEW_TAB_TEMPLATE_DEFAULT;
}
};
})();
const getStrengthStepPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default strength step.");
settingsUnavailableLogged = true;
}
return STRENGTH_STEP_DEFAULT;
}
try {
const value = settingManager.get(STRENGTH_STEP_SETTING_ID);
return value ?? STRENGTH_STEP_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read strength step setting, using default.", error);
settingsUnavailableLogged = true;
}
return STRENGTH_STEP_DEFAULT;
}
};
})();
const getLoraActiveFiltersAutocompletePreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default lora active filters autocomplete setting.");
settingsUnavailableLogged = true;
}
return LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT;
}
try {
const value = settingManager.get(LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID);
return value ?? LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read lora active filters autocomplete setting, using default.", error);
settingsUnavailableLogged = true;
}
return LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT;
}
};
})();
// Listeners fired when the node label preference changes, so open graphs can
// relabel their rows without waiting for a reload.
const nodeLoraNameDisplayListeners = new Set();
/**
* Register a callback fired when the node LoRA label preference changes.
* Returns an unsubscribe function.
*/
export function onNodeLoraNameDisplayChanged(callback) {
nodeLoraNameDisplayListeners.add(callback);
return () => {
nodeLoraNameDisplayListeners.delete(callback);
};
}
const notifyNodeLoraNameDisplayChanged = () => {
for (const listener of nodeLoraNameDisplayListeners) {
try {
listener();
} catch (error) {
console.warn("LoRA Manager: node LoRA label listener failed", error);
}
}
};
const getNodeLoraNameDisplayPreference = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, labelling node LoRA rows with file names.");
settingsUnavailableLogged = true;
}
return NODE_LORA_NAME_DISPLAY_DEFAULT;
}
try {
const value = settingManager.get(NODE_LORA_NAME_DISPLAY_SETTING_ID);
return value === NODE_LORA_NAME_DISPLAY_OPTION_MODEL_NAME ? "model_name" : NODE_LORA_NAME_DISPLAY_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read node LoRA label setting, labelling rows with file names.", error);
settingsUnavailableLogged = true;
}
return NODE_LORA_NAME_DISPLAY_DEFAULT;
}
};
})();
// ============================================================================
// Register Extension with All Settings
// ============================================================================
app.registerExtension({
name: "LoraManager.Settings",
settings: [
{
id: TRIGGER_WORD_WHEEL_SENSITIVITY_ID,
name: "Trigger Word Wheel Sensitivity",
type: "slider",
attrs: {
min: 0.01,
max: 0.1,
step: 0.01,
},
defaultValue: TRIGGER_WORD_WHEEL_SENSITIVITY_DEFAULT,
tooltip: "Mouse wheel sensitivity for adjusting trigger word strength (default: 0.02)",
category: ["LoRA Manager", "Trigger Word Toggle", "Wheel Sensitivity"],
},
{
id: AUTO_PATH_CORRECTION_SETTING_ID,
name: "Auto path correction",
type: "boolean",
defaultValue: AUTO_PATH_CORRECTION_DEFAULT,
tooltip: "Automatically update model paths to their current file locations.",
category: ["LoRA Manager", "Automation", "Auto path correction"],
},
{
id: PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
name: "Enable Tag Autocomplete in Prompt Nodes",
type: "boolean",
defaultValue: PROMPT_TAG_AUTOCOMPLETE_DEFAULT,
tooltip: "When enabled, typing in a Prompt (LoraManager) node triggers tag autocomplete suggestions. You can also toggle it by typing /autocomplete or /noautocomplete in the node, or from the node's right-click menu. Slash commands (e.g., /character, /artist) always work regardless of this setting.",
category: ["LoRA Manager", "Autocomplete", "Prompt"],
},
{
id: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
name: "Search LoRA autocomplete within active filters",
type: "boolean",
defaultValue: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT,
tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. You can also toggle it by typing /activefilters or /noactivefilters in the LoRA field, or from the node's right-click menu.",
category: ["LoRA Manager", "Autocomplete", "LoRA Active Filters"],
},
{
id: AUTOCOMPLETE_APPEND_COMMA_SETTING_ID,
name: "Append comma after autocomplete",
type: "boolean",
defaultValue: AUTOCOMPLETE_APPEND_COMMA_DEFAULT,
tooltip: "When enabled, accepted autocomplete suggestions append ', ' to the inserted text.",
category: ["LoRA Manager", "Autocomplete", "Append comma"],
},
{
id: AUTOCOMPLETE_AUTO_FORMAT_SETTING_ID,
name: "Auto format autocomplete text on blur",
type: "boolean",
defaultValue: AUTOCOMPLETE_AUTO_FORMAT_DEFAULT,
tooltip: "When enabled, leaving an autocomplete textarea removes duplicate commas and collapses unnecessary spaces.",
category: ["LoRA Manager", "Autocomplete", "Auto Format"],
},
{
id: AUTOCOMPLETE_ACCEPT_KEY_SETTING_ID,
name: "Autocomplete accept key",
type: "combo",
options: [
AUTOCOMPLETE_ACCEPT_KEY_OPTION_BOTH,
AUTOCOMPLETE_ACCEPT_KEY_OPTION_TAB_ONLY,
AUTOCOMPLETE_ACCEPT_KEY_OPTION_ENTER_ONLY,
],
defaultValue: AUTOCOMPLETE_ACCEPT_KEY_OPTION_BOTH,
tooltip: "Choose which key accepts the selected autocomplete suggestion. Keys not selected here keep their normal textarea behavior.",
category: ["LoRA Manager", "Autocomplete", "Accept key"],
},
{
id: TAG_SPACE_REPLACEMENT_SETTING_ID,
name: "Replace underscores with spaces in tags",
type: "boolean",
defaultValue: TAG_SPACE_REPLACEMENT_DEFAULT,
tooltip: "When enabled, tag names with underscores will have them replaced with spaces when inserted (e.g., 'blonde_hair' becomes 'blonde hair').",
category: ["LoRA Manager", "Autocomplete", "Tag Formatting"],
},
{
id: USAGE_STATISTICS_SETTING_ID,
name: "Enable usage statistics tracking",
type: "boolean",
defaultValue: USAGE_STATISTICS_DEFAULT,
tooltip: "When enabled, LoRA Manager will track model usage statistics during workflow execution. Disabling this will prevent unnecessary disk writes.",
category: ["LoRA Manager", "Statistics", "Usage Tracking"],
},
{
id: ANIMA_LORA_BLOCK_REMAP_SETTING_ID,
name: "Legacy Anima LoRA block remap",
type: "boolean",
defaultValue: ANIMA_LORA_BLOCK_REMAP_DEFAULT,
tooltip: "Automatically remap LoRA block indices when an older Anima LoRA is loaded on Anima 2.9B (40 blocks) or 3.8B (52 blocks). Turn off to load the file unchanged.",
category: ["LoRA Manager", "LoRA Loader", "Anima Block Remap"],
onChange: async (value) => {
if (!animaLoraBlockRemapSynced) {
return;
}
await persistAnimaLoraBlockRemap(value);
},
},
{
id: NEW_TAB_TEMPLATE_ID,
name: "New Tab Template Workflow",
type: "combo",
options: getWorkflowOptions,
defaultValue: NEW_TAB_TEMPLATE_DEFAULT,
tooltip: "Choose a template workflow to load when creating a new workflow tab. 'Default (Blank)' keeps ComfyUI's original blank workflow behavior.",
category: ["LoRA Manager", "Workflow", "New Tab Template"],
},
{
id: STRENGTH_STEP_SETTING_ID,
name: "Strength Adjustment Step",
type: "slider",
attrs: {
min: 0.01,
max: 0.1,
step: 0.01,
},
defaultValue: STRENGTH_STEP_DEFAULT,
tooltip: "Step size for adjusting LoRA strength via arrow buttons or keyboard (default: 0.05)",
category: ["LoRA Manager", "LoRA Widget", "Strength Step"],
},
{
id: NODE_LORA_NAME_DISPLAY_SETTING_ID,
name: "Node LoRA label",
type: "combo",
options: [
NODE_LORA_NAME_DISPLAY_OPTION_FILE_NAME,
NODE_LORA_NAME_DISPLAY_OPTION_MODEL_NAME,
],
defaultValue: NODE_LORA_NAME_DISPLAY_OPTION_FILE_NAME,
tooltip: "Label LoRA rows on the LoRA Loader, Stacker, Create Hook LoRA, Pool, Randomizer and WanVideo Lora Select nodes with the on-disk file name or with the library model name. Model names shared by several files gain a distinguishing prefix.",
category: ["LoRA Manager", "LoRA Widget", "Node LoRA Label"],
onChange: () => {
notifyNodeLoraNameDisplayChanged();
},
},
],
async setup() {
await loadWorkflowOptions();
await syncAnimaLoraBlockRemapFromBackend();
const originalNewBlankWorkflow = async () => {
const blankGraph = {
last_node_id: 0,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
extra: {},
version: 0.4,
};
await app.loadGraphData(blankGraph);
};
const waitForCommandStore = async (maxWaitMs = 5000) => {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
if (app.extensionManager?.command?.commands) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return false;
};
const selectLoraManagerSettings = async (maxWaitMs = 2000) => {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const navItem = document.querySelector(
'[data-testid="settings-dialog"] [data-nav-id="LoRA Manager"], [data-testid="settings-dialog"] [data-nav-id$="/LoRA Manager"]'
);
if (navItem) {
navItem.click();
return;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
};
const openSettingsDialog = async () => {
try {
if (await waitForCommandStore()) {
await app.extensionManager.command.execute("Comfy.ShowSettingsDialog");
await selectLoraManagerSettings();
return;
}
if (typeof app?.ui?.settings?.show === "function") {
app.ui.settings.show();
await selectLoraManagerSettings();
return;
}
console.warn("LoRA Manager: Could not access the ComfyUI settings dialog.");
} catch (error) {
console.warn("LoRA Manager: Failed to open the ComfyUI settings dialog.", error);
}
};
const patchCommand = async () => {
const storeReady = await waitForCommandStore();
if (!storeReady) {
console.warn("LoRA Manager: Could not access command store to patch NewBlankWorkflow");
return;
}
const commands = app.extensionManager.command.commands;
for (const cmd of commands) {
if (cmd.id === "Comfy.NewBlankWorkflow") {
const originalFunc = cmd.function;
cmd.function = async (metadata) => {
const templateName = getNewTabTemplatePreference();
if (templateName && templateName !== NEW_TAB_TEMPLATE_DEFAULT) {
const workflowData = await loadTemplateWorkflow(templateName);
if (workflowData) {
// Override the workflow's saved view settings with our custom zoom
if (!workflowData.extra) {
workflowData.extra = {};
}
if (!workflowData.extra.ds) {
workflowData.extra.ds = { offset: [0, 0], scale: 1 };
}
workflowData.extra.ds.scale = NEW_TAB_ZOOM_LEVEL;
await app.loadGraphData(workflowData);
return;
}
}
await originalNewBlankWorkflow();
};
break;
}
}
};
window.addEventListener("message", (event) => {
if (event.origin !== window.location.origin || event.data?.type !== OPEN_COMFYUI_SETTINGS_MESSAGE) {
return;
}
void openSettingsDialog();
});
const tabId = window.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`;
let lastActive = Date.now();
window.addEventListener("focus", () => {
lastActive = Date.now();
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
lastActive = Date.now();
}
});
if (typeof window.BroadcastChannel === "function") {
try {
const channel = new window.BroadcastChannel(COMFYUI_SETTINGS_CHANNEL);
channel.addEventListener("message", (event) => {
const data = event.data;
if (data?.type === DISCOVER_COMFYUI_SETTINGS_MESSAGE) {
channel.postMessage({
type: COMFYUI_SETTINGS_AVAILABLE_MESSAGE,
requestId: data.requestId,
tabId,
lastActive,
});
} else if (data?.type === OPEN_COMFYUI_SETTINGS_MESSAGE && data.targetTabId === tabId) {
try {
window.focus();
} catch (_) {
}
void openSettingsDialog();
}
});
} catch (error) {
console.warn("LoRA Manager: Existing-tab settings navigation is unavailable.", error);
}
}
const currentUrl = new URL(window.location.href);
if (currentUrl.searchParams.get(OPEN_COMFYUI_SETTINGS_QUERY) === "1") {
currentUrl.searchParams.delete(OPEN_COMFYUI_SETTINGS_QUERY);
window.history.replaceState(window.history.state, "", `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`);
void openSettingsDialog();
}
patchCommand();
},
});
// ============================================================================
// Exports
// ============================================================================
export {
OPEN_COMFYUI_SETTINGS_MESSAGE,
OPEN_COMFYUI_SETTINGS_QUERY,
COMFYUI_SETTINGS_CHANNEL,
DISCOVER_COMFYUI_SETTINGS_MESSAGE,
COMFYUI_SETTINGS_AVAILABLE_MESSAGE,
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
getWheelSensitivity,
getAutoPathCorrectionPreference,
getAutocompleteAppendCommaPreference,
getAutocompleteAutoFormatPreference,
getAutocompleteAcceptKeyPreference,
getPromptTagAutocompletePreference,
getTagSpaceReplacementPreference,
getUsageStatisticsPreference,
getNewTabTemplatePreference,
getStrengthStepPreference,
getLoraActiveFiltersAutocompletePreference,
getNodeLoraNameDisplayPreference,
setLoraManagerSettingValue,
};