Add missing-file detection with click-to-replace
Rows pointing at a lora that's no longer on disk (moved, renamed, or deleted) now render red with a warning marker instead of only logging to the console at generation time. The missing row's name is click-to-replace: clicking it reopens the folder browser to swap in a working file in place. Detection reuses the existing /tree endpoint - its full paths are flattened into a set of valid loras, and a row is flagged when its lora isn't in that set. Fails safe (flags nothing until the list is known / if the fetch fails) and tolerates path-separator differences across OSes. Documents the feature in the README with a screenshot and promotes it out of the roadmap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
02534c279e
commit
51899bb266
@@ -20,6 +20,7 @@ The node in its default state, right after adding it to the canvas:
|
||||
- **Native quick-edit dialogs** — click any strength or priority number to get ComfyUI's own auto-selected value-entry dialog (matches the interaction rgthree's nodes use)
|
||||
- **Toggle-all / delete-all** — bulk controls in the header row, column-aligned with the per-row toggle and trash icons
|
||||
- **Auto-growing node** — the node resizes itself to fit your lora list; shrink it manually below that and the list scrolls internally instead of spilling out
|
||||
- **Missing-file detection** — if a row points at a lora that's no longer on disk (moved, renamed, or deleted since the workflow was saved), the row is flagged in red with a ⚠ marker, and its name becomes click-to-replace so you can swap in a working file without rebuilding the row
|
||||
|
||||
A populated stack — drag handle, priority number, toggle, name, strength stepper, and trash icon, all column-aligned with the header row's toggle-all and delete-all controls:
|
||||
|
||||
@@ -33,6 +34,10 @@ Connect `clip` and every row gains an independent second stepper — the header
|
||||
|
||||

|
||||
|
||||
If a row's lora file goes missing — moved, renamed, or deleted since the workflow was saved — that row is flagged in red with a ⚠ marker instead of silently failing at generation time. Click the highlighted name to reopen the folder browser and pick a replacement in place:
|
||||
|
||||

|
||||
|
||||
## Install
|
||||
|
||||
**Via git clone (recommended):**
|
||||
@@ -95,7 +100,6 @@ Planned features, roughly in the order they're likely to land:
|
||||
**Self-contained (no external services needed):**
|
||||
- Search/filter box in the Add Lora popup — type to filter instead of scrolling through folders
|
||||
- Duplicate detection — block re-adding a lora that's already in the stack, with a clear message, since there's no real use case for having the same file twice
|
||||
- Missing-file detection — if a saved workflow references a lora that's been moved or deleted, highlight that row (rather than only printing to the console), and offer a way to search for it
|
||||
|
||||
**Trigger words, suggested strength, and CivitAI metadata (one combined feature):**
|
||||
- A clickable info icon after the lora name, showing trigger words and CivitAI metadata for that lora
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -43,6 +43,21 @@ if (!document.getElementById(STYLE_ID)) {
|
||||
border-color: #6d5aa8;
|
||||
}
|
||||
|
||||
/* Missing on disk - wins over enabled-highlight's background/border so a
|
||||
broken row reads as broken whether it's toggled on or off. */
|
||||
.fll-row.missing,
|
||||
.fll-row.missing.enabled-highlight {
|
||||
background: #3a2323;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
.fll-name.fll-name-missing {
|
||||
color: #ff8a80;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fll-name.fll-name-missing:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --- Toggle switch (replaces checkbox) --- */
|
||||
.fll-toggle-switch {
|
||||
position: relative;
|
||||
@@ -397,6 +412,46 @@ let _treeCache = null;
|
||||
let _treeCacheTime = 0;
|
||||
const TREE_CACHE_MS = 15000; // refresh at most every 15s so new files show up reasonably fast
|
||||
|
||||
// Flat set of every valid lora path the backend reports (the exact strings
|
||||
// get_filename_list("loras") returns, which is what an entry's `lora` field
|
||||
// stores). Stays null until the first successful tree fetch so we never flag
|
||||
// rows as "missing" before we actually know what's on disk.
|
||||
let validLoraPaths = null;
|
||||
|
||||
function _normLora(p) {
|
||||
// Normalize separators so a workflow saved on one OS doesn't read as
|
||||
// "missing" on another; the flag is purely cosmetic so being lenient here
|
||||
// only ever avoids false positives.
|
||||
return String(p).replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
// Walk the nested tree collecting every value in every __full_paths__ map -
|
||||
// those values are the full relative paths, i.e. the source of truth for
|
||||
// what's installed. Rebuilt on each tree fetch so added/removed files show up.
|
||||
function _rebuildValidLoraPaths(tree) {
|
||||
const set = new Set();
|
||||
(function walk(node) {
|
||||
if (!node || typeof node !== "object") return;
|
||||
const fp = node.__full_paths__;
|
||||
if (fp) {
|
||||
for (const k in fp) set.add(_normLora(fp[k]));
|
||||
}
|
||||
for (const key in node) {
|
||||
if (key === "__files__" || key === "__full_paths__") continue;
|
||||
walk(node[key]);
|
||||
}
|
||||
})(tree);
|
||||
validLoraPaths = set;
|
||||
}
|
||||
|
||||
// True only when we have a loaded list AND this lora isn't in it. Unknown
|
||||
// (list not loaded yet / fetch failed) -> false, so we fail safe toward
|
||||
// "not missing" rather than lighting up every row red on a transient hiccup.
|
||||
function isLoraMissing(loraName) {
|
||||
if (!validLoraPaths || !loraName) return false;
|
||||
return !validLoraPaths.has(_normLora(loraName));
|
||||
}
|
||||
|
||||
async function fetchLoraTree(force = false) {
|
||||
const now = Date.now();
|
||||
if (!force && _treeCache && now - _treeCacheTime < TREE_CACHE_MS) {
|
||||
@@ -407,6 +462,7 @@ async function fetchLoraTree(force = false) {
|
||||
const data = await res.json();
|
||||
_treeCache = data;
|
||||
_treeCacheTime = now;
|
||||
_rebuildValidLoraPaths(data);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error("[UltimateLoraLoader] Failed to fetch lora tree", e);
|
||||
@@ -571,7 +627,7 @@ function openLoraBrowser(x, y, onSelect) {
|
||||
// Node widget
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeRow(entry, { onChange, onRemove, showClipStrength, onDragStart, onDragOver, onDrop, onDragEnd, onPriorityChange, currentPriority }) {
|
||||
function makeRow(entry, { onChange, onRemove, onReplace, showClipStrength, onDragStart, onDragOver, onDrop, onDragEnd, onPriorityChange, currentPriority }) {
|
||||
const row = document.createElement("div");
|
||||
row.className = rowClassName(entry);
|
||||
// Row itself isn't draggable - only the dedicated drag handle icon
|
||||
@@ -660,8 +716,23 @@ function makeRow(entry, { onChange, onRemove, showClipStrength, onDragStart, onD
|
||||
const name = document.createElement("div");
|
||||
name.className = "fll-name";
|
||||
const shortName = entry.lora.split("/").pop();
|
||||
name.textContent = shortName;
|
||||
name.title = entry.lora;
|
||||
const missing = isLoraMissing(entry.lora);
|
||||
name.textContent = missing ? "⚠ " + shortName : shortName;
|
||||
if (missing) {
|
||||
name.classList.add("fll-name-missing");
|
||||
name.title = `File not found on disk:\n${entry.lora}\n\nClick to choose a replacement.`;
|
||||
// Click-to-replace: reopen the same folder browser and swap this row's
|
||||
// lora in place. Cheap stand-in for the roadmap's "click-to-search"
|
||||
// until a real search box exists.
|
||||
name.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (!onReplace) return;
|
||||
const rect = name.getBoundingClientRect();
|
||||
openLoraBrowser(rect.left, rect.bottom + 4, (loraPath) => onReplace(loraPath));
|
||||
};
|
||||
} else {
|
||||
name.title = entry.lora;
|
||||
}
|
||||
|
||||
const STRENGTH_STEP = 0.05;
|
||||
|
||||
@@ -855,6 +926,7 @@ function rowClassName(entry) {
|
||||
let cls = "fll-row";
|
||||
if (!entry.enabled) cls += " disabled";
|
||||
if (entry.enabled) cls += " enabled-highlight";
|
||||
if (isLoraMissing(entry.lora)) cls += " missing";
|
||||
return cls;
|
||||
}
|
||||
|
||||
@@ -1074,6 +1146,11 @@ app.registerExtension({
|
||||
render();
|
||||
resizeNode();
|
||||
},
|
||||
onReplace: (loraPath) => {
|
||||
entry.lora = loraPath;
|
||||
persist();
|
||||
render();
|
||||
},
|
||||
showClipStrength,
|
||||
currentPriority: idx + 1,
|
||||
onPriorityChange: (rawPriority) => {
|
||||
@@ -1373,6 +1450,13 @@ app.registerExtension({
|
||||
render();
|
||||
syncWidth();
|
||||
|
||||
// Fetch the on-disk lora list so missing-file highlighting can light up
|
||||
// once we know what actually exists. The paint above happens first and
|
||||
// flags nothing (validLoraPaths is null until this resolves); this
|
||||
// re-render applies red highlighting to any rows pointing at files that
|
||||
// are no longer present.
|
||||
fetchLoraTree().then(() => render());
|
||||
|
||||
// restore size sanity on load (also re-syncs width once ComfyUI has
|
||||
// finished laying out the node on the canvas, in case initial
|
||||
// node.size wasn't final yet at widget-creation time). Also retry
|
||||
|
||||
Reference in New Issue
Block a user