fix(models): wires onto core ports can be removed again; readable node shelf
A wire onto an open core port is recorded twice — as the slot's wire and as a
core_override — and reconcileOpenPortWiring() rebuilds each from the other. Removing
the wire cleared only the first, so the next reconcile put it straight back. That is
why "Studio · model" could not be unwired without deleting the node. _setWireTarget
now drops the override too, and only when it still names that same wire.
Confirmed both ways: with a stale override alone, reconcile resurrects the wire.
Cards now derive their name from the file the node loads, so five LoRAs read as
add-detail-xl / filmgrain_v2 instead of five nodes called "LoRA". An explicit rename
still wins. Each card also says where it feeds ("→ Studio · model"), using the same
derived names, so the shelf shows how the pipeline is put together.
Tiles are capped rather than 1fr — in a narrow pane two stretched columns made each
card ~270px tall.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ef9f7d02f7
commit
2c0b08bdee
@@ -489,9 +489,36 @@
|
||||
if (n != null) { slot.role_label = n.trim() || undefined; await persist(); render(); }
|
||||
}
|
||||
|
||||
// Which widget on a node names the file it loads. Checked in order, then any
|
||||
// remaining combo input whose name ends in _name — enough to cover core loaders and
|
||||
// most third-party ones without a per-class table to keep up to date.
|
||||
const FILE_INPUTS = [
|
||||
"lora_name", "unet_name", "ckpt_name", "checkpoint_name", "model_name", "clip_name",
|
||||
"vae_name", "control_net_name", "style_model_name", "gguf_name", "upscale_model_name",
|
||||
];
|
||||
|
||||
// Five LoRAs all called "LoRA" are indistinguishable, and renaming each by hand is the
|
||||
// tax this avoids: derive the name from the file the node actually loads.
|
||||
function autoLabel(slot) {
|
||||
const inputs = slot.inputs || {};
|
||||
const spec = specFor(slot);
|
||||
let key = FILE_INPUTS.find((k) => typeof inputs[k] === "string" && inputs[k]);
|
||||
if (!key) {
|
||||
const combos = (spec?.inputs || []).filter((i) => i.kind === "combo" && /_name$/.test(i.name));
|
||||
key = combos.map((i) => i.name).find((k) => typeof inputs[k] === "string" && inputs[k]);
|
||||
}
|
||||
if (!key) return null;
|
||||
const raw = String(inputs[key]);
|
||||
// "SDXL/detail/add-detail-xl.safetensors" -> "add-detail-xl"
|
||||
const base = raw.split(/[\\/]/).pop().replace(/\.[a-z0-9]+$/i, "");
|
||||
return base || null;
|
||||
}
|
||||
|
||||
function slotDisplayLabel(slot) {
|
||||
const role = roles.find((r) => r.key === slot.role);
|
||||
return slot.role_label || (role ? role.label : slot.role) || slotName(slot) || "?";
|
||||
// An explicit rename always wins; the parsed filename only fills the gap where the
|
||||
// label would otherwise be the role, which every node of that role shares.
|
||||
return slot.role_label || autoLabel(slot) || (role ? role.label : slot.role) || slotName(slot) || "?";
|
||||
}
|
||||
|
||||
// ── node page (one configured slot: values, wiring, sources) ──────────────────
|
||||
@@ -851,6 +878,19 @@
|
||||
}
|
||||
// Replace a wire target + mirror it as an input source on the destination slot.
|
||||
function _setWireTarget(srcSlot, outName, oldTarget, newTarget) {
|
||||
// A wire onto an open core port is recorded TWICE: as the slot's wire, and as a
|
||||
// core_override. reconcileOpenPortWiring() rebuilds each from the other, so dropping
|
||||
// only the wire left the override behind and the next reconcile put the wire straight
|
||||
// back — which is why "Studio · model" could not be unwired without deleting the node.
|
||||
if (String(oldTarget || "").startsWith("port:")) {
|
||||
const map = portToOpenCore(oldTarget.slice(5));
|
||||
if (map) {
|
||||
const [cid, inp] = map;
|
||||
// Only if the override still names THIS wire; another output may own it now.
|
||||
if (config.core_overrides?.[cid]?.[inp] === `out:${srcSlot.id}:${outName}`)
|
||||
delete config.core_overrides[cid][inp];
|
||||
}
|
||||
}
|
||||
const old = _parseNodeTarget(oldTarget);
|
||||
if (old) {
|
||||
const ds = slotById(old.slotId);
|
||||
@@ -1616,10 +1656,51 @@
|
||||
const meta = el("div", "node-card-meta");
|
||||
meta.append(el("div", "node-card-title", slotDisplayLabel(slot)));
|
||||
meta.append(el("div", "node-card-sub", slotName(slot)));
|
||||
|
||||
// Where this node feeds — otherwise the shelf shows what you have but not how it is
|
||||
// put together, and you have to open every card to find that out.
|
||||
const outs = wireDestinationLabels(slot);
|
||||
const flow = el("div", "node-card-flow");
|
||||
if (!outs.length) {
|
||||
flow.classList.add("none");
|
||||
flow.append(el("span", null, "→ not wired"));
|
||||
} else {
|
||||
flow.append(el("span", null, "→ " + outs[0]));
|
||||
if (outs.length > 1) flow.append(el("span", "node-card-more", `+${outs.length - 1} more`));
|
||||
}
|
||||
meta.append(flow);
|
||||
card.append(meta);
|
||||
return card;
|
||||
}
|
||||
|
||||
// Human label for one wire target ("port:…", "node:<id>:<input>", "global:…").
|
||||
function targetLabel(value) {
|
||||
if (!value) return null;
|
||||
if (value === "global:video") return "Global video output";
|
||||
if (value === "global:audio") return "Global audio output";
|
||||
if (value.startsWith("port:")) {
|
||||
const p = ports.find((x) => "port:" + x.id === value);
|
||||
return p ? p.label : value.slice(5);
|
||||
}
|
||||
const n = _parseNodeTarget(value);
|
||||
if (n) {
|
||||
const dest = slotById(n.slotId);
|
||||
return dest ? `${slotDisplayLabel(dest)} · ${n.input}` : n.input;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function wireDestinationLabels(slot) {
|
||||
const out = [];
|
||||
Object.values(slot.wires || {}).forEach((raw) => {
|
||||
wireTargets(raw).forEach((t) => {
|
||||
const l = targetLabel(t);
|
||||
if (l && !out.includes(l)) out.push(l);
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function nodeGrid(v) {
|
||||
const wrap = el("div", "node-grid");
|
||||
|
||||
|
||||
@@ -2347,8 +2347,10 @@ body.tour-mode { overflow: hidden; }
|
||||
is "+ New node", so adding lives where the nodes are rather than in a
|
||||
sidebar you have to know about. */
|
||||
.node-grid {
|
||||
display: grid; gap: 14px; margin: 4px 0 22px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||
display: grid; gap: 14px; margin: 4px 0 22px; justify-content: start;
|
||||
/* A capped max, not 1fr: in a narrow pane two 1fr columns stretched each tile to
|
||||
~270px tall. A shelf keeps its tile size and reflows the count instead. */
|
||||
grid-template-columns: repeat(auto-fill, minmax(116px, 132px));
|
||||
}
|
||||
.node-card {
|
||||
display: flex; flex-direction: column; gap: 8px; padding: 0;
|
||||
@@ -2356,7 +2358,7 @@ body.tour-mode { overflow: hidden; }
|
||||
font-family: inherit; color: var(--text);
|
||||
}
|
||||
.node-card-art {
|
||||
position: relative; aspect-ratio: 3 / 4; border-radius: 10px; overflow: hidden;
|
||||
position: relative; aspect-ratio: 1 / 1; border-radius: 10px; overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(160deg, var(--ink-3), var(--ink-1));
|
||||
border: 1px solid var(--line);
|
||||
@@ -2371,10 +2373,10 @@ body.tour-mode { overflow: hidden; }
|
||||
.node-card.bad .node-card-art { border-color: color-mix(in srgb, var(--danger) 60%, var(--line)); }
|
||||
|
||||
.node-card-mark {
|
||||
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.4; font-weight: 500;
|
||||
color: var(--muted); text-align: center; padding: 10px 12px;
|
||||
font-family: var(--font-mono); font-size: 11px; line-height: 1.35; font-weight: 500;
|
||||
color: var(--muted); text-align: center; padding: 8px 9px;
|
||||
overflow-wrap: anywhere; hyphens: auto;
|
||||
display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
|
||||
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.node-card:hover .node-card-mark { color: var(--text); }
|
||||
.node-card-plus { font-size: 40px; line-height: 1; color: var(--accent); font-weight: 300; }
|
||||
@@ -2419,3 +2421,15 @@ body.tour-mode { overflow: hidden; }
|
||||
.node-foot-hint { flex: 1; min-width: 160px; font-size: 11.5px; color: var(--faint); }
|
||||
.node-foot-acts { display: flex; gap: 8px; }
|
||||
.node-foot .btn.primary.disabled, .node-foot .btn.primary:disabled { opacity: .4; cursor: default; }
|
||||
|
||||
/* Where a node feeds, on the card itself — the shelf should show how the pipeline is
|
||||
put together, not just what is in it. */
|
||||
.node-card-flow {
|
||||
display: flex; align-items: baseline; gap: 5px; margin-top: 3px;
|
||||
font-size: 10.5px; line-height: 1.35; color: var(--teal);
|
||||
}
|
||||
.node-card-flow > span:first-child {
|
||||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.node-card-flow.none { color: var(--faint); }
|
||||
.node-card-more { flex: none; color: var(--faint); }
|
||||
|
||||
Reference in New Issue
Block a user