Releases: Deaththegrim/ComfyUI-PromptLibrary
v0.55.2 — hand-pass VRAM ceiling + Queue ▶▶ per-gallery
Detailer
- Hands preset crop_factor 2.0 → 1.5. At hires the hand bbox already has plenty of pixels; the 2x context made the upscaled crop near-fullres and UNet sample + VAE on that latent was the highest VRAM peak in detail(). Was close to locking up 16 GB cards even with v0.55.0's diet. Per-target override still works.
- Tiled VAE encode/decode threshold 1024 → 768. More crops go tiled instead of risking the non-tiled peak. Tiled retry already starts at tile=256, so the cost is bounded.
Library UI
- Queue ▶▶ drives THIS gallery's parent node instead of the first PromptLibrary in the graph. Multi panel can now cycle its own panel (others fixed); Style node can cycle styles. Widget values are saved/restored around the loop so multi-select stays in sync after the run.
- Multi-select persistence fix: skip the 'prune missing IDs' pass when
promptsis empty during initial onConfigure render — without the guard, a freshly-restored selection got wiped on first render.
v0.55.0 — VRAM diet + lockup mitigation
What's new
Detailer and sampler hardening targeting an AMD/HIP hard-freeze fingerprint observed under multi-pass detailer runs (gfxhub page faults → VRAM exhaustion → wedge).
Smart Detailer
- Per-bbox soft_empty_cache removed — flushing the allocator every iteration defeated pooling and forced HIP alloc/free roundtrips (the wedge driver). Per-pass flush retained.
- fp32→fp16 cast hoisted to detail() so subsequent passes reuse the owned buffer instead of cloning per-pass (saves ~100-200 MB at hires resolution per pass).
- sam_mask_cache pruned between passes — drops bboxes no future pass will reuse. A 6-pass disjoint-detection run no longer holds full-resolution mask tensors for the whole call.
- SAM predictor offloads to CPU at end of detail(), reclaiming ~600 MB-1.5 GB VRAM between runs without invalidating the model cache.
Samplers (SDXL + Anima)
- Tile-step-down OOM retry for pixel-upscale-with-model and VAE encode/decode (256 → 128 → 64) instead of crashing.
- sampler_sdxl frees
decodedandupscaledbetween hires iterations (~200 MB of dead weight per iteration). - sampler_anima defragments allocator between sample and decode under fragmentation pressure.
Tests
+9 unit tests covering the new offload helper and the cache-pruning algorithm. 333 pass, 5 skip, 1 deselected (pre-existing unrelated comic_page test).
v0.53.1 — Smart Detailer mouth/feet targets + widget reorder
v0.53.1 — Smart Detailer mouth + feet targets, library persistence
Smart Detailer
Two new targets
| Target | Default crop | Wildcard |
|---|---|---|
| mouth/teeth | 2.5 | "detailed mouth, clean teeth, sharp lips," |
| feet/shoes | 2.5 | "detailed feet, sharp shoe details, clean stitching," |
Pass order is now face → skin → mouth → eyes → feet → hands — mouth runs after skin so the freshly-detailed face is the input for teeth/lip work, eyes run after mouth so the iris detail lands on the cleaned-up face.
The per-target grid UI grew from 4 to 6 columns. Default node width bumped to 480 so the grid fits.
Widget order rationalised
Required widget order is now logically grouped:
enable_face, enable_eyes, enable_mouth, enable_hands, enable_feet, enable_skin
bbox_face, bbox_eyes, bbox_mouth, bbox_hands, bbox_feet, sam_model
seed, steps, cfg, sampler_name, scheduler, denoise, ...
This is a breaking widget-shift relative to v0.53.0. Two fixes:
- Run
python3 tools/fix_workflow_widgets.py <workflow.json> -oto migrate saved workflows in place (with.bak) - Or right-click the Smart Detailer node → Remove → re-add fresh
The migration tool's widget order list was updated to match.
Quality-default sweep
Already shipped at v0.52.1 but worth restating since these defaults travel with the node:
| Widget | New default | Was |
|---|---|---|
steps |
25 | 20 |
denoise |
0.45 | 0.40 |
guide_size |
1024 | 512 |
max_size |
1536 | 1024 |
bbox_threshold |
0.45 | 0.5 |
yolo_imgsz |
960 | 640 |
Library
Selection now persists across page refreshes via belt-and-suspenders:
- Primary store:
idWidget.value(comma-joined ids, ComfyUI auto-serialises) - Backup mirror:
node.properties[propsKey].selectedIds(always serialised regardless of widget-value handling) syncFromWidgetfalls back to the properties mirror when the widget value comes back empty on restore
applySelectionChange now calls writeState on every selection mutation so the mirror is always fresh.
Recommended detector models
Drop into models/ultralytics/bbox/:
- face:
Anzhc Face seg 640 v4 y11n.pt(HF:Anzhc/Anzhcs_YOLOs) — YOLO11n, stylised-friendly - eyes:
Anzhc Eyes -seg-hd.pt(HF:Anzhc/Anzhcs_YOLOs) — anime sclera-tuned - mouth: Civitai 1306938 v1.0 — YOLO11n illustration mouth
- hands: Civitai 329458 v9c — YOLOv9c, catches partial/off-angle hands the v8s version misses
- feet: Civitai 2511165 v0.4-seg — YOLO11n foot+shoe segmentation
v0.52.1 — Smart Detailer per-target grid UI + quality defaults
v0.52.1 — Smart Detailer: per-target grid UI + quality defaults
UI
Replaces the long vertical stack of 16+4 per-target widgets (enable / threshold / denoise / max / steps × face / eyes / hands / skin) with a compact 5-column grid:
FACE EYES HANDS SKIN
enable [☐] [☐] [☐] [☐]
threshold [-1.00] [-1.00] [-1.00] [-1.00]
denoise [-1.00] [-1.00] [-1.00] [-1.00]
max N [ 0] [ 0] [ 0] [ 0]
steps [ 0] [ 0] [ 0] [ 0]
- Column headers colour-coded to match the bbox preview: face=green, skin=yellow, eyes=cyan, hands=magenta.
- "Use global" sentinel values (
-1for floats,0for ints) render dimmed-italic so overrides are visually obvious. - Underlying widgets stay in
node.widgetsfor serialisation — hidden behind the DOM panel via the same pattern Multi Library uses. Workflows saved against v0.52.0 load fine. - Default node width bumped to 380 so the grid fits cleanly.
Quality-optimum defaults
| Widget | Old | New | Why |
|---|---|---|---|
steps |
20 | 25 | cleaner sampling |
denoise |
0.40 | 0.45 | enough detail without identity drift |
guide_size |
512 | 1024 | SDXL-native resolution |
max_size |
1024 | 1536 | room for SDXL crops to breathe |
bbox_threshold |
0.5 | 0.45 | catches partial / off-angle faces |
yolo_imgsz |
640 | 960 | catches background / small faces |
Tooltips updated to call out "quality default" and document the speed-vs-quality knob direction for each widget.
v0.52.0 — Smart Detailer + no-pack-deps cleanup
v0.52.0 — Smart Detailer + no-pack-deps cleanup
What's new
🔬 GrimmRibbity Smart Detailer (new node)
Self-contained one-node face/eyes/hands/skin detailer. Replaces the 3-node Impact-Pack FaceDetailer chain with target toggles, in-node YOLO loading by filename, optional SAM mask refinement, and ComfyUI's built-in sampler. Zero ComfyUI custom-node deps — uses ultralytics + sam2 / segment_anything from PyPI directly.
Six rounds of perf optimisation:
- Round 1 — SAM
set_imagehoisted to per-target, conditional tiled VAE decode (full for ≤1024px, tiled above), ProgressBar across all bboxes, single image-to-uint8 conversion shared YOLO+SAM+preview, empty-detection fast path. - Round 2 — module-level CLIP encode LRU (survives across calls), edge-aware feathering (no rect seam at image borders),
mask_strengthwidget, optionaltiled_encode, hot-path imports hoisted,same_seed_per_targettoggle. - Round 3 — YOLO
model.fuse()(Conv+BN, 10–20% inference speedup), explicit GPU pin, per-pass wall-time logging. - Round 4 — SAM2 batched-prompt predict (one forward instead of N for crowd scenes), lazy preview clone,
nms_iouwidget,yolo_imgszwidget. - Round 5 — per-target steps overrides, text-labelled bboxes on preview output, SAM mask cache shared across passes (face+skin stop double-SAMing identical bboxes).
- Round 6 — SAM v1 batched predict via
predict_torch(parity with SAM2), soft-cancel between bboxes,draw_previewtoggle,max_bbox_area_pctsanity cap on whole-image false positives.
NaN-scrub on every refined-image path so downstream nodes never see NaN → uint8 cast garbage. Outputs: image, mask, detections_preview.
🧹 No-pack-deps cleanup
- Character Anchor — IPAdapter Plus import is lazy at
anchor()call time. Module loads cleanly without IPAdapter installed; node always visible in the picker. - SDXL Sampler — dropped efficiency-nodes city96/ttl_nn neural latent upscalers. Legacy strings retained on the dropdown so saved workflows still validate; runtime falls back to bicubic with a console warning.
- Detailer — idempotently registers
ultralytics_bbox/segm/samsfolder paths so dropdowns work even without Impact Subpack installed.
🛡️ Code-scanning fixes (CodeQL)
py/mismatched-multiple-assignment— explicit 8-tuple in_unpack_sdxl_tuple.js/xss-through-dom— addedf.type.startsWith("image/")guard beforeURL.createObjectURLin the upload handler.py/multiple-definition— removed deadpending_sincevariable from the watcher loop.py/overly-permissive-file— prompt-log fd mode0o644→0o600(owner-only).py/unused-import× 3 — dropped unusedtimefromcivitai_save.py+tools/screenshots/run.py,Pathfromtests/test_civitai_save.py.
🎨 Web + theme
- Smart Detailer (#d8754a burnt orange) and Character Anchor (#8eaa3e olive) added to
NODE_COLORS. - Selection-state diagnostic logging in
promptLibrary.jsfor the multi-Library ghost-select bug investigation; toggle off viawindow._plDebugSelect = false.
✅ Tests
299 passing (up from 263). New tests/test_detailer.py covers helpers, bypass/passthrough, INPUT_TYPES shape, edge-aware feather mask, wildcard cache LRU, conditional tiled-decode policy, font drawer.
Install
Clone or unzip into your ComfyUI custom_nodes/ directory and restart ComfyUI. The Smart Detailer node appears under right-click → Add Node → GrimmRibbity → Detailer → Smart Detailer.
Requires ultralytics (already installed via Impact Pack typically). Optional: sam2 or segment_anything for mask refinement.
v0.47.1 — HiResFix round 5 + Civitai batch lift + Wildcard IS_CHANGED
v0.47.1 — Round-5 HiResFix wins + cross-node perf sweep
Folded changes since v0.46.2.
v0.47.0 — HiResFix round 5
| What changed | Real-world impact | |
|---|---|---|
| Soft GPU cleanup between iterations | New _between_iterations_cleanup() calls comfy.model_management.soft_empty_cache() after every iteration. Gentler than torch.cuda.empty_cache() — only fires when memory is actually constrained. |
Long iteration loops (4–5 iters at 4× scale) used to accumulate VRAM fragmentation; iteration 5 sometimes allocated around fragments instead of into them. Stable now. |
denoise=0 short-circuit |
common_ksampler at denoise=0 still ran its noise+step setup ritual (~1 s per call) even though no sampling happens. The call is now skipped entirely; the upscaled latent flows through unchanged. |
Saves ~1 s × N iterations × stages on "upscale-only" workflows. Both samplers. |
Per-pass hires_sampler / hires_scheduler |
Two new optional widgets on the SDXL HiResFix Script. (same as primary) default = inherits the primary call (no behaviour change). Override to use a different sampler at the hires stage. |
Useful for "primary dpmpp_3m_sde + karras + hires euler + simple" patterns, or swapping to faster samplers when hires steps are cheap. |
v0.47.1 — Cross-node perf sweep
| What changed | Impact | |
|---|---|---|
| Civitai Save: lift batch-shared work out of the per-frame loop | build_a1111_parameters + json.dumps(prompt) + json.dumps(extra_pnginfo) were called per frame inside the save loop. Frames in a Comfy batch always share H/W, so the parameter string is identical for every frame; same for the workflow-trace JSON. |
Batch of 4 with a typical trace: ~3 redundant param-string builds + 3 redundant JSON dumps avoided per save. Compounds across overnight batches. |
| Wildcard Expand IS_CHANGED tracks library mtime | Same staleness pattern as the Random by Tag fix in v0.42.1. With expand_named_refs=True, the result depends on whichever entries __name__-references resolve to. Edits to those entries mid-session must invalidate Comfy's cached output. mtime folded into the hash only on the named-ref branch — pure {a|b|c} expansion still hashes fast. |
Edits to library entries now actually re-trigger downstream Wildcard Expand consumers at fixed seed. |
Audit notes — verified no fix needed
| Node | Status |
|---|---|
| Scene / Background / Comic Frame | Pure-string composition; Comfy's default input-hash IS_CHANGED is correct |
| Save / ThumbnailSaver | OUTPUT_NODE=True, side-effect writers — should re-run every queue |
| Multi node | IS_CHANGED already hashes picked entries' text |
| Library node | IS_CHANGED hashes per-entry text + negative + LoRA sig (when MODEL/CLIP wired) |
| Style / Comic Page / Character Anchor | Required-input nodes, Comfy hashes inputs by default |
| LoRA Picker | Stateless (per-call session-cached saves) |
Cumulative HiResFix optimization picture (v0.32 → v0.47.0)
16 HiResFix-specific items shipped across 5 rounds. Every per-iteration cost that can be hoisted has been; every load that can be cached is; every redundant work step short-circuits when not needed.
Backward compat
All changes internal — no socket / widget / workflow JSON shape changes since v0.46.2. Workflow JSONs from any version since the optional-MODEL/CLIP refactor (v0.32) still load cleanly.
Install / upgrade
cd ComfyUI/custom_nodes/ComfyUI-GrimmRibbity
git pull
# restart ComfyUI / hard-refresh browser tabv0.46.2 — Sticky-prompt_id hardening + half-wired warning
v0.46.2 — Sticky-prompt_id hardening + Library half-wired warning
Triggered by a user report: overwrite_by_name on the Save node was "sticking" and overwriting the latest entry instead of the named one. Library scan showed zero duplicate names, so the most likely cause was a sticky prompt_id widget masking overwrite_by_name. Two patch releases folded into this tag.
v0.46.1 — overwrite_by_name + modal collision
| What changed | |
|---|---|
overwrite_by_name picks most-recently-updated |
Was first-by-storage-order = oldest dup. Now matches "the one I last edited" intent. Collapses to the single match when no dupes. |
| Save node logs the lookup path | Every Save now prints via=prompt_id / via=overwrite_by_name / via=new to the Comfy console. The diagnostic for sticky widget values — if every save logs via=prompt_id even when the user expected overwrite_by_name to fire, the Save node's prompt_id field has a pinned value to clear. |
Modal +Add warns on duplicate name |
Was silently appending _2 / _3 (the actual mechanism that lets users accumulate same-named entries). Now pops a "Save as new / Cancel and edit existing" confirm. |
v0.46.2 — ThumbnailSaver + Library half-wired warning
| What changed | |
|---|---|
| ThumbnailSaver success log | PromptLibraryThumbnailSaver has the same prompt_id widget as the Save node — same risk class. Previously logged only failures; now also logs success: [ThumbnailSaver] wrote thumbnail to id='foo' name='Foo Style'. |
| Library half-wired MODEL/CLIP warning | Wiring just MODEL or just CLIP into the Library node silently skipped the LoRA path (both required). Now prints a clear warning so the user knows why their LoRAs aren't applying. |
Audit notes — no fix needed
- Style node has MODEL + CLIP as required, so half-wired is structurally impossible.
- Multi-panel node uses
prompt_id_<N>widgets driven by the gallery, hidden, cleaned on node delete (v0.37); not at risk. - All other
next()lookups are byid, which is unique by construction (_unique_id+_safe_id).
How to diagnose the original bug
Restart Comfy, run your Save-node workflow once, check the console:
[PromptLibrary] saved id='X' name='X' via=prompt_id ← sticky prompt_id
[PromptLibrary] saved id='X' name='X' via=overwrite_by_name ← working as intended
[PromptLibrary] saved id='X' name='X' via=new ← created new
If via=prompt_id when you expected overwrite_by_name, the Save node's prompt_id field has a pinned value. Clear it.
Tests
263 total (was 261, +2). The new cases cover overwrite_by_name picking the newest match and creating-when-no-match.
Backward compat
All changes internal — no socket / widget / workflow JSON shape changes.
Install / upgrade
cd ComfyUI/custom_nodes/ComfyUI-GrimmRibbity
git pull
# restart ComfyUI / hard-refresh browser tabv0.46.0 — Gallery validator + 11 rounds of audit polish
v0.46.0 — Gallery-wired library validator + 11 rounds of audit polish
The cumulative changeset since v0.39 (the last tagged release). Eleven rounds of audit-driven work, 34 items shipped, 2 silent data-loss / curation bugs surfaced, 261 tests (was 242, +19), 3 new standalone CLI tools. All changes internal — no socket / widget / workflow JSON shape changes.
v0.46.0 highlights — library validator wired into the gallery
The gallery now surfaces library-health issues directly in the toolbar. A new ⚠ N badge appears only when there are unhealed issues (clean libraries get no UI clutter). Click it to open a categorised modal:
- Broken LoRA references — entries whose
loraslist references a.safetensorsno longer inmodels/loras/. Per-row Edit button opens the entry so you can re-pick or delete the row. - Orphan thumbnails — image files with no matching entry. Single "Remove orphan thumbnails" action calls the new
POST /prompt_library/fix_orphansendpoint. - Invalid entry IDs — entries whose id doesn't match
[A-Za-z0-9_-]{1,64}(would refuse to upsert). - Empty-text entries — entries with no prompt text from a half-completed save.
Two new HTTP routes:
GET /prompt_library/validate— read-only findings dictPOST /prompt_library/fix_orphans— bulk-delete stray thumbnails
The badge re-fetches after every gallery refresh so the count stays accurate. Network failures hide the badge silently — the gallery itself works whether the validator endpoint is reachable or not.
Round J — drag insertion line + library_validate CLI
- Drag-reorder visual — the previous "outline the target tile" gave an ambiguous "replace or sit next to?" affordance. Now a 3px accent-coloured line on the leading edge of the target tile (top edge in list view) literally shows where the dragged tile will land.
tools/library_validate.py— the same checks as the gallery validator, runnable from the shell. Read-only by default;--fix-orphansdeletes stray thumbnails.
Round I — search + modal usability
- Gallery search now matches LoRA fields + negative + notes — used to walk only
name / text / tags / id. A query liketurbo(LoRA filename) orgreen eyes(trigger word) used to miss matching entries. - Visible missing-LoRA warning — modal LoRA rows whose path isn't in the loaded list now show a red banner above the dropdown, not just a
(missing)marker the user has to click the dropdown to see. PromptLibrarySaveloras_jsoninput — workflows can now save entries with attached LoRAs programmatically. Empty input leaves existing loras untouched;[]explicitly clears.
Earlier rounds (v0.40 → v0.45)
| Round | What landed |
|---|---|
| A v0.40.0 | zip-bomb cap, CSV cap, /list directory cache, JSON validation, atomic prompt_log writes |
| B v0.40.1 | SHA cache LRU, named depth constants, deep-copy loras |
| C v0.40.2 | end-to-end roundtrip tests + fix negative field silently lost on every export → import |
| D v0.41.0 | dual M/C strength sliders + LoRA-row soft-delete + Scene empty-output warning |
| E v0.42.0 | Comic Page per-panel panel_strengths override + cycle test + loras-in-record test |
| F v0.42.1 | LoRA Picker port discovery, Random IS_CHANGED tracking, watcher try/except resilience |
| G v0.43.0 | tools/civitai_backfill.py CLI |
| H v0.44.0 | snapshot path-traversal hardening, watcher coalesce, modal try/catch, gallery lazy-render via requestIdleCallback, strength link-toggle |
Two real bugs found by the audit
- Negative-field round-trip leak (Round C) — every export → import lost the entry's negative prompt. Found by the new end-to-end test.
- Empty-text "Cyberpunk" entry (Round J/K) — surfaced by the validator running against the maintainer's live 718-entry library; would never have been noticed otherwise.
Backward compat
All changes internal — no socket / widget / workflow JSON shape changes since v0.39. Drop-in safe.
Install / upgrade
cd ComfyUI/custom_nodes/ComfyUI-GrimmRibbity
git pull
# restart ComfyUI / hard-refresh browser tabThree standalone CLI tools (in tools/)
# Diagnose library issues (read-only by default)
python3 tools/library_validate.py [--fix-orphans]
# Backfill Civitai SHA hashes onto legacy PNGs
python3 tools/civitai_backfill.py /path/to/output [--recursive] [--dry-run]
# Capture screenshots of the live gallery (since v0.36)
python3 tools/screenshots/run.pyv0.43.0 — Audit-driven polish + Civitai backfill CLI
v0.43.0 — Six rounds of audit-driven polish + Civitai backfill CLI
A condensed release covering grind-rounds A → G from a systematic codebase audit. Highlights below; no socket / widget / workflow JSON shape changes since v0.41.
Round A — security + perf wins
- Zip-bomb defence on
/import_zip— sums uncompressed sizes from each ZipInfo header before reading any member; rejects expansions over 2 GB. - CSV size cap (50 MB) on
/import_csv— was unbounded; now consistent with image (16 MB) and zip (500 MB) caps. /listdirectory cache —_image_path_forwas doing ~7 stat() calls per entry inside the list response (4900 syscalls for a 700-entry library per/list). Now oneos.scandirbuilds an id set per request.- JSON body validation — new
_json_payloadhelper centralises the inconsistent pattern across 5 POST routes; malformed bodies → 400 with a clear message instead of 500. Adds missingids must be a listcheck to/exportand/bulk_delete. - Atomic
prompt_logwrites — singleos.write(fd, line_bytes)+ fsync per JSONL line. Process crash mid-write can't corrupt the file anymore.
Round B — quality / consistency
- SHA256 hash-cache LRU capped at 4096 entries (~320 KB on disk).
- Named depth constants (
_TEXT_LINK_MAX_DEPTH,_PIPE_TRACE_MAX_DEPTH) replace bare4/8literals in the Civitai workflow walker. - Deep-copy loras on duplicate — was a shallow
[dict(l) for l in src["loras"]].
Round C — test coverage + a real bug
- End-to-end save → export → reimport round-trip test — caught a silent data-loss bug: the
negativefield was being exported but never read back on import. Fixed alongside. - New tests for
import_backgroundsandimport_tag_packsroutes. - 247 tests, was 242.
Round D — modal UX
- Independent CLIP-strength slider in the Edit Prompt modal — each LoRA row now has stacked
MandCslider+number pairs. Backend always trackedstrength_modelandstrength_clipseparately; the UI was silently mirroring them. Pre-v0.40.3 entries with onlystrength_modelsaved →strength_clipdefaults to the model value on load (mirror preserved as fallback). - Soft-delete on LoRA rows — Delete now toggles to "Restore"; row dims and strikes through but stays editable. Misclick is reversible without abandoning every other modal edit.
- Scene-node empty-output warning —
PromptLibraryScenewith every knob at(none)and emptyextraused to silently emit"". Now logs.
Round E — Comic Page + more tests
- Per-panel strength override on Comic Page (Regional) via a new optional
panel_strengthsCSV widget. Position N maps to panel N; blank/missing positions fall back to the global default. - 5 new PerPanelStrengthTests, 1 new WalkModelChainCycleTests, 1 new build_record-with-loras test.
- 254 tests, was 247.
Round F — small misc items
- LoRA Picker auto-detects ComfyUI's host:port from
PromptServer.instance.serverinstead of hardcoded127.0.0.1:8188. PromptLibraryRandom.IS_CHANGEDnow folds the prompts.json mtime into the cache key, so editing a tag-matched entry mid-session invalidates the cached pick at fixed seed too.- Watcher loop wraps each iteration in
try/except Exceptionso a future bug in_notify_changecan't kill the daemon and break gallery auto-refresh for the rest of the session.
Round G — Civitai metadata backfill CLI
python3 tools/civitai_backfill.py /path/to/output --recursive --verboseWalks a folder of PNGs, reads the workflow JSON Comfy embeds in the prompt chunk, computes SHA256 hashes for the model + LoRAs (cached on disk in the same data/hash_cache.json the live node uses), and rewrites the parameters chunk with a proper Civitai Hashes: {...} JSON block. Idempotent — already-tagged PNGs are skipped on re-run. Useful for a backlog of generations from before the Save node was wired in.
Designed to run independently of ComfyUI — no live server needed, no Python deps beyond Pillow.
Backward compat
All changes internal — no socket / widget / workflow JSON shape changes. Drop-in safe with v0.41.x and v0.42.x workflows.
Install / upgrade
cd ComfyUI/custom_nodes/ComfyUI-GrimmRibbity
git pull
# restart ComfyUI / hard-refresh browser tabv0.39.0 — Gallery event delegation
v0.39.0 — Gallery event delegation
The last big perf hit on huge libraries: every tile attached its own click / contextmenu / dragstart / dragend / dragover / dragleave / drop handlers, plus a checkbox.onclick on the inner element. For a 700-tile library in Manual sort mode that created ~4900 handler closures per render, each capturing checkedIds / syncWidget / render / refresh / openPromptModal / etc. via lexical scope. Render-then-discard cycle hit GC hard on every filter or sort change.
What changed
Replaced with a single set of grid-level delegated handlers attached once per gallery instance. Each delegated handler:
- resolves the affected tile via
e.target.closest('.pl-tile') - reads
dataset.promptId - looks up the entry via
prompts.find/lastVisible.findIndex - routes to the same logic as before
Five new helpers (private to buildGallery)
_entryFromTile(tile) → entry from prompts[]
_indexFromTile(tile) → index in lastVisible[]
_toggleSelectionAt(id, ...) → plain + shift-range click
_openAddPrompt() → '+' tile click handler
_openTileContextMenu(e, p) → right-click menu builder
Per-tile creation cost
| Operation | Before (per tile) | After (per tile) |
|---|---|---|
createElement + setAttrs |
yes | yes |
| Closure constructions | 7–9 | 0 |
| Inner-DOM build | yes | yes |
For 700 tiles in Manual sort: ~4900 closures NOT created on render.
Cumulative gallery perf wins (v0.36 → v0.39)
| Action on 700-entry library | v0.36 | v0.39 |
|---|---|---|
| Type one search character | full grid rebuild + 4900 closures | debounced 80 ms + DocumentFragment + 0 closures |
| Click a tile to select | full grid rebuild + 4900 closures | one tile's classList toggled |
| Shift-range select 100 tiles | full grid rebuild + 4900 closures | 100 tiles' classes toggled |
| Arrow-key navigation | full grid rebuild + 4900 closures per keystroke | 2 DOM mutations per keystroke |
| Bulk clear selection | full grid rebuild + 4900 closures | N tiles' classes removed |
| Sort change | full grid rebuild + 4900 closures | full rebuild + 0 closures (correct order needed) |
| Right-click for context menu | per-tile closure already there | one delegated handler per gallery |
Search-as-you-type, click-to-select, and arrow-key nav on 500–700 entry libraries should feel instant. Every interaction except a genuine reorder is now O(few-DOM-mutations) instead of O(visible-tiles).
Backward compat
All changes internal — no socket / widget / workflow JSON shape changes.
Install / upgrade
cd ComfyUI/custom_nodes/ComfyUI-GrimmRibbity
git pull
# restart ComfyUI / hard-refresh browser tab