From 77c159a91875de80638614ead81c2b8830002b72 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 21 Jun 2026 14:49:40 +0200 Subject: [PATCH] feat(search): side preview panel for mirror search + strip stray NUL byte Splits the palette into results (left) + a preview panel (right) that updates on hover / arrow-key navigation, modern-search style. Since disabled-pack nodes have no loaded definition (can't render a real node graphic), the panel shows the pack metadata we do have: title, author, description, repo link, version, and the sibling nodes in the pack (active node highlighted). Enable 7d / Enable available in both the row and the panel. buildDisabledCatalog now attaches a shared per-pack meta object (from getmappings entry[1]) to each catalog entry. Also removes a literal NUL byte that had slipped into the dedup separator string, which made grep treat the file as binary; replaced with a newline. Bump to 1.5.0. --- README.md | 16 ++++--- js/nodes_stats.js | 103 ++++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- 3 files changed, 107 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 957cc76..90d7beb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A ComfyUI custom node package that silently tracks which nodes, packages, and mo - **Expandable detail** — click any package to see individual node-level stats - **One-click disable** — disable unused packages straight from the dialog via ComfyUI Manager (per-package or in bulk), reversible at any time - **Workflow tab** — on loading a workflow, splits unresolved nodes into *Missing* (install via Manager) and *Disabled*, with a temporary **Enable 7d** trial that auto-disables packages left unused -- **Mirror search** — a standalone palette (⌕ button / `Ctrl/Cmd+Shift+D`) that searches nodes belonging to currently-disabled packages and re-enables them on the spot +- **Mirror search** — a standalone palette (⌕ button / `Ctrl/Cmd+Shift+D`) that searches nodes belonging to currently-disabled packages, previews the owning package + its sibling nodes, and re-enables them on the spot - **Non-blocking** — DB writes happen in a background thread, no impact on workflow execution ## Package Classification @@ -119,15 +119,21 @@ package* and lets you re-enable the owning package right from the results. **`Ctrl/Cmd+Shift+D`** (ignored while typing in an input). - Type to filter — results are ranked (node-name prefix first, then word-start, substring, finally pack-name matches) and show the `class_type` and its pack. +- Hover a result (or use ↑/↓) to open a **preview panel** on the right with the + owning package's title, author, description, repo link, and the full list of + sibling nodes in that pack — the active node highlighted. (A true rendered node + graphic isn't possible here: the pack is disabled, so ComfyUI hasn't loaded the + node's slot definition; the panel shows the package metadata we do have.) - Each result offers **Enable 7d** (re-enable under a 7-day trial) and **Enable** - (re-enable permanently) — the same enable path as the Workflow tab. + (re-enable permanently) — in the row and in the preview panel — the same enable + path as the Workflow tab. - Enabling takes effect after a ComfyUI restart; enabled rows mark *"✓ enabled · restart"*. The catalog is built once per session by joining ComfyUI Manager's node→pack -mappings with the list of disabled packs, and cached; use the **↻** button to -rebuild it. The palette is inert (with a clear message) when ComfyUI Manager is -absent or there are no disabled packages. +mappings with the disabled packs (matched across dir name, registry id, and repo +URL), and cached; use the **↻** button to rebuild it. The palette is inert (with +a clear message) when ComfyUI Manager is absent or there are no disabled packages. **Models tab** - Summary bar with counts for each tier across all model types diff --git a/js/nodes_stats.js b/js/nodes_stats.js index 59672bc..03d8666 100644 --- a/js/nodes_stats.js +++ b/js/nodes_stats.js @@ -546,19 +546,36 @@ function buildDisabledCatalog(mappings, managerInfo) { } const catalog = []; const seen = new Set(); + const packMeta = {}; // dir -> shared { pack, title, author, description, repo, version, info, nodes:[] } for (const [packKey, entry] of Object.entries(mappings || {})) { const rec = byAnyKey[normalizeRepoUrl(packKey)]; if (!rec || rec.info.state !== "disabled") continue; const list = entry && entry[0]; if (!Array.isArray(list)) continue; - const title = rec.info.title || rec.dir; + const m = (entry && entry[1]) || {}; + let meta = packMeta[rec.dir]; + if (!meta) { + const repo = (rec.info.files || []).find((f) => /^https?:\/\//i.test(f)) || ""; + meta = packMeta[rec.dir] = { + pack: rec.dir, + title: m.title || m.title_aux || rec.info.title || rec.dir, + author: m.author || "", + description: m.description || "", + repo, + version: rec.info.version || "", + info: rec.info, + nodes: [], + }; + } for (const ct of list) { - const dedup = rec.dir + "" + ct; + const dedup = rec.dir + "\n" + ct; if (seen.has(dedup)) continue; seen.add(dedup); - catalog.push({ class_type: ct, pack: rec.dir, title, info: rec.info }); + meta.nodes.push(ct); + catalog.push({ class_type: ct, pack: rec.dir, title: meta.title, info: rec.info, meta }); } } + for (const meta of Object.values(packMeta)) meta.nodes.sort((a, b) => a.localeCompare(b)); return catalog; } @@ -868,42 +885,105 @@ async function openMirrorSearch() { const box = document.createElement("div"); box.style.cssText = - "margin-top:10vh;background:#1e1e1e;color:#ddd;border:1px solid #444;border-radius:8px;width:90%;max-width:640px;max-height:70vh;display:flex;flex-direction:column;font-family:monospace;font-size:13px;overflow:hidden;"; + "margin-top:10vh;background:#1e1e1e;color:#ddd;border:1px solid #444;border-radius:8px;width:90%;max-width:880px;max-height:70vh;display:flex;flex-direction:column;font-family:monospace;font-size:13px;overflow:hidden;"; box.innerHTML = `
-
+
+
+
+
`; overlay.appendChild(box); document.body.appendChild(overlay); const input = box.querySelector("#ns-mirror-input"); const results = box.querySelector("#ns-mirror-results"); + const preview = box.querySelector("#ns-mirror-preview"); const footer = box.querySelector("#ns-mirror-footer"); + let currentRows = []; + let activeIndex = -1; + + function clearPreview(msg) { + preview.innerHTML = `
${escapeHtml(msg || "Hover a result to preview its package.")}
`; + } + + // Preview panel for the active row. We can't render a real node graphic (the + // pack is disabled, so its definition isn't loaded), so we show the pack + // metadata we do have: title/author/description + the sibling nodes in the pack. + function renderPreview(entry) { + if (!entry) { clearPreview(); return; } + const m = entry.meta || {}; + const sibs = m.nodes || []; + const CAP = 60; + const shown = sibs.slice(0, CAP); + const sibHtml = shown.map((n) => { + const me = n === entry.class_type; + return `
${me ? "▸ " : "· "}${escapeHtml(n)}
`; + }).join("") + (sibs.length > shown.length ? `
+${sibs.length - shown.length} more
` : ""); + const meta = [`pack${escapeHtml(entry.pack)}`]; + if (m.author) meta.push(`author${escapeHtml(m.author)}`); + if (m.version) meta.push(`version${escapeHtml(String(m.version))}`); + preview.innerHTML = ` +
${escapeHtml(entry.class_type)}
+
${meta.join("")}
+ ${m.description ? `
${escapeHtml(m.description)}
` : ""} + ${m.repo ? `
${escapeHtml(m.repo)}
` : ""} +
+ + +
+
${sibs.length} node${sibs.length !== 1 ? "s" : ""} in this pack
+
${sibHtml}
`; + preview.querySelectorAll(".ns-mirror-temp").forEach((b) => + b.addEventListener("click", () => mirrorEnable(b.dataset.pkg, true, overlay))); + preview.querySelectorAll(".ns-mirror-perm").forEach((b) => + b.addEventListener("click", () => mirrorEnable(b.dataset.pkg, false, overlay))); + } + + function setActive(i) { + if (!currentRows.length) { activeIndex = -1; clearPreview(); return; } + activeIndex = Math.max(0, Math.min(i, currentRows.length - 1)); + const els = results.querySelectorAll(".ns-mrow"); + els.forEach((el, idx) => el.classList.toggle("active", idx === activeIndex)); + els[activeIndex]?.scrollIntoView({ block: "nearest" }); + renderPreview(currentRows[activeIndex]); + } + footer.textContent = "loading disabled-node catalog…"; + clearPreview("Loading…"); let catalog = await ensureDisabledCatalog(); - if (catalog === null) { footer.textContent = "ComfyUI Manager not available."; return; } - if (catalog.length === 0) { footer.textContent = "No disabled packages — nothing to search."; return; } + if (catalog === null) { footer.textContent = "ComfyUI Manager not available."; clearPreview(" "); return; } + if (catalog.length === 0) { footer.textContent = "No disabled packages — nothing to search."; clearPreview(" "); return; } const packCount = new Set(catalog.map((e) => e.pack)).size; footer.textContent = `${catalog.length} nodes across ${packCount} disabled packs · enabling needs a restart`; function render() { const { rows, total } = filterCatalog(catalog, input.value); + currentRows = rows; + activeIndex = -1; if (!input.value.trim()) { results.innerHTML = `
Type to search ${catalog.length} nodes in ${packCount} disabled packs.
`; + clearPreview(); + return; + } + if (total === 0) { + results.innerHTML = `
No disabled nodes match “${escapeHtml(input.value)}”.
`; + clearPreview("No match."); return; } - if (total === 0) { results.innerHTML = `
No disabled nodes match “${escapeHtml(input.value)}”.
`; return; } let html = ""; for (const e of rows) { html += `
@@ -921,9 +1001,16 @@ async function openMirrorSearch() { b.addEventListener("click", () => mirrorEnable(b.dataset.pkg, true, overlay))); results.querySelectorAll(".ns-mirror-perm").forEach((b) => b.addEventListener("click", () => mirrorEnable(b.dataset.pkg, false, overlay))); + results.querySelectorAll(".ns-mrow").forEach((el, i) => + el.addEventListener("mouseenter", () => setActive(i))); + setActive(0); } input.addEventListener("input", render); + input.addEventListener("keydown", (e) => { + if (e.key === "ArrowDown") { e.preventDefault(); setActive(activeIndex + 1); } + else if (e.key === "ArrowUp") { e.preventDefault(); setActive(activeIndex - 1); } + }); box.querySelector("#ns-mirror-refresh").addEventListener("click", async () => { footer.textContent = "refreshing…"; catalog = await ensureDisabledCatalog(true) || []; diff --git a/pyproject.toml b/pyproject.toml index c6fb75e..89f4f5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-nodes-stats" description = "Track usage statistics for all ComfyUI nodes and packages" -version = "1.4.1" +version = "1.5.0" license = "MIT" [project.urls]