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:
digital-garbage
2026-08-12 18:47:43 +03:00
co-authored by Claude Opus 5
parent ef9f7d02f7
commit 2c0b08bdee
2 changed files with 102 additions and 7 deletions
+82 -1
View File
@@ -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");
+20 -6
View File
@@ -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); }