Lands the broader save-to-library rethink across the server + extension
surfaces. Closes Bugs 1-5 from the 2026-05-19 live QA report; Bug 6
(payload validation) was already plugged by the previous commit.
API contract (Phase B):
- PUT /wp/api/modules/{id} now accepts {payload, name, description,
tags, propagate_to_bundles} and persists all of them in one
transaction. Returns bundles_updated[] so the caller can surface
"this save touched N bundles" feedback.
- PUT /wp/api/modules/{id}/payload kept as a backwards-compat alias
that mirrors the same body shape (payload + meta + propagate). The
older route silently dropped meta; now it routes through the same
helpers as the canonical PUT.
- Bundle propagation: when a module write happens, every bundle whose
children[].id matches the updated module gets its frozen child
snapshot rewritten in lockstep (payload + meta) and its
payload_hash recomputed. Atomic via shared db_session.
- GET /wp/api/bundles?contains_module=<id> filters the list to bundles
that reference that module — drives the modal's "this save affects
N bundles" preview.
- 7 new pytest cases in test_save_to_library_unified.py covering meta
persistence on both PUT routes, opt-in/opt-out propagation, and the
bundles filter.
SPA (Phase C):
- New PushToLibraryModal.vue. Owns editable name/description/tags +
payload preview + bundles-affected list + propagate checkbox. Two
buttons: Save as new entry (always available — POST /modules) and
Update existing (greyed when payload_hash is empty). Replaces the
five per-kind onXxxSaveToLibraryClick handlers + their five copies
of the same PUT body that lived in ModuleEditModal.
- ContextWidget right-click menu entry becomes Push to library and
routes to the same modal. Predicate relaxed from
isMissingFromLibrary(m) to !!m.payload, so inline-created modules
(no payload_hash) can finally reach the library — they get the
fork-only path in the modal.
- The five instance modals canSaveToLibrary computeds drop the
isLibraryTracked + isModified gate. Button is visible whenever
there is a payload; the modal decides what to do with it. Tests
updated to match the new contract.
- 8 new vitest cases in PushToLibraryModal.test.ts covering field
seeding, button disabling, PUT/POST request shape, bundle list
rendering, and the propagate opt-out round-trip.
- Legacy saveToLibrary(m) function in ContextWidget is gone (no
callers since the menu predicate change). Its bypass POST to
import-from-workflow lost the meta-editing path the new modal
exposes, so deletion is net positive.
Size note: total extension bundle is now 306254 / 307200 bytes
gzipped — 946 bytes of headroom on the size gate. Future feature
work touching this surface needs to factor in budget.
Live QA pending: extension code needs ComfyUI restart to reload.