From 2c0b08bdeeaa2112759bbf00922cca296cb3bbde Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:47:43 +0300 Subject: [PATCH] fix(models): wires onto core ports can be removed again; readable node shelf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- movie_editor/frontend/models.js | 83 +++++++++++++++++++++++++++++++- movie_editor/frontend/styles.css | 26 +++++++--- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index 951fd6d3..f33fe613 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -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::", "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"); diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index b56b33d7..1edfb5a4 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -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); }