Commit 454210a4 replaced renderFunction() with widget.value setter +
widget.callback() in endDrag, so the test assertion should verify
callback invocation instead of the removed renderSpy call.
- Ernie & Anima: auto-fetched via CivitaiBaseModelService from Civitai API
- Ernie Turbo & Nucleus: pre-added as hardcoded constants (not yet in Civitai API)
- Added abbreviations (ERNI, ETRB, NUCL) and category entries across all layers
Replaces two separate menu items with a single smart item that dynamically
switches between 'Set as Favorite' and 'Remove from Favorites' based on
whether all selected items are already favorited. Shows a count badge
'(3/5)' when only some items are favorited in a mixed selection.
Supports all model types (LoRA, Checkpoint, Embedding) and recipes via
existing per-item save/update API — no backend changes needed.
The model-level API (GET /api/v1/models/{id}) does not include usageControl
on version entries, causing generation-only models to show as downloadable.
Backend changes:
- Add get_model_versions_by_hashes() to CivitaiClient (POST by-hash batch)
- Propagate through all provider classes including RateLimitRetryingProvider
- Add _enrich_version_entries() pipeline: extract SHA256 from files[].hashes,
batch-call by-hash endpoint, inject usageControl+earlyAccessEndsAt in-place
- Wire enrichment into both bulk (_fetch_model_versions_bulk) and individual
(_refresh_single_model) refresh paths
- Fix _build_record_from_remote dropping usage_control field
- Fix POST by-hash request format (plain JSON array, not {hashes:[...]} object)
Frontend changes:
- Fix disabled download button tooltip: wrap in <span> since HTML title
attribute does not fire on disabled elements
- batch-import-modal.css: add generic font family fallback to Font Awesome
- card.css: remove dead margin-left overridden by shorthand margin: 0
- shared.css: remove duplicate position: absolute overridden by position: fixed
- Remove transform: translateY(-1px) that caused layout shift on focus
- Reduce box-shadow focus ring from 2px to 1px for subtler appearance
- Tone down drop-shadow from 4px/16px to 2px/8px (matches base state)
Moved wiki-images to the wiki repo (willmiao/ComfyUI-Lora-Manager.wiki). Updated README.md image reference to use wiki raw URL. Removed docs/LM-Extension-Wiki.md (superseded by wiki pages).
During drag, handleStrengthDrag is called with updateWidget=false, which
mutates widgetValue in-place via parseLoraValue's direct array reference,
bypassing widget.value setter and options.setValue entirely.
endDrag only called renderFunction for a DOM refresh, but never flushed the
mutation through options.setValue. Any external observer that wraps
options.setValue (e.g. ComfyUI Mirror Panel's bidirectional sync) would
therefore never see the dragged value and would treat the widget as unchanged.
Fix: replace the explicit renderFunction call with widget.value = widget.value.
This flushes the in-place mutation through the setter (options.setValue), which
re-renders the DOM internally AND notifies all setValue wrappers. Also fire
widget.callback for parity with the updateWidget=true path in handleStrengthDrag.
Applies the same fix to initHeaderDrag (proportional all-LoRA header drag).
Five entry points that trigger recipe page reloads were not passing
preserveScroll: true, causing the page to snap back to top after
filtering, searching, or navigating folders — especially painful with
hundreds of recipes.
- RecipePageControls.resetAndReload() → refreshVirtualScroll() now
passes { preserveScroll: true } (sidebar folder clicks/drag moves)
- FilterManager applyFilters/clearAllFilters → loadRecipes(true)
changed to loadRecipes({ preserveScroll: true })
- SearchManager performSearch → loadRecipes(true) changed to
loadRecipes({ preserveScroll: true })
- SettingsManager reloadContent → loadRecipes() changed to
loadRecipes({ preserveScroll: true })
The normalizeLoadRecipesOptions boolean path always forces
preserveScroll: false — the object form is required to pass it.
- Use get_lora_info_absolute to obtain correct absolute paths for loras
in LM extra folder paths, instead of folder_paths.get_full_path which
only searches ComfyUI's standard loras directories (returned None)
- Fix name field truncation: str.split('.')[0] stopped at the first dot,
replaced with os.path.splitext to only strip the file extension
- Add _relpath_within_loras helper to preserve subdirectory info in the
name field, matching WanVideoWrapper's os.path.splitext(lora)[0] format
New endpoint: GET /api/lm/check-models-exist?modelIds=1,2,3,...
Accepts comma-separated modelIds, returns a results array with one
entry per modelId. Uses a single scanner lookup batch - three
service-registry calls total, regardless of model count. Skips
history checks entirely (same rationale as the singleton endpoint:
when models exist locally, history is redundant).
Expected: reduces 231 HTTP round-trips to 1 for the browser
extension's model-card indicator flow. Combined with the prior
SQLite-connection and history-skip fixes, total wall-clock time
for a 175K-lora user's page load drops from ~9.4s to <10ms.
Root cause: 231 concurrent /check-model-exists requests on 175K-lora library
caused ~9.4s wall clock time. The bottleneck was two-fold:
1. DownloadedVersionHistoryService opened a new sqlite3.connect() for every
query under asyncio.Lock. With a large WAL from 175K entries, each
connect() took ~8ms. Serialized by the lock across 231 requests, the
230th request waited ~1848ms just for lock acquisition.
2. check_model_exists always queried download history even when the model
was found locally. The history result (hasBeenDownloaded /
downloadedVersionIds) is only used by the UI when the model is NOT
found locally; when found, the 'in library' indicator takes priority.
Changes:
- downloaded_version_history_service.py: added persistent _get_conn() that
creates the SQLite connection once and reuses it across all queries
- misc_handlers.py: early-return from check_model_exists when the model
exists locally, bypassing the history service entirely (lock skipped)
Expected: per-request wait time drops from ~1912ms to <3ms, wall clock
from ~9.4s to <0.3s for the 175K-lora user's 231-card page.
- Add dynamic column calculation based on container width and min card width
- Prevent tiny cards on narrow windows by respecting density-based minimums:
- Default: 240px, Medium: 200px, Compact: 170px
- Fix edge-to-edge layout with proper CSS selector (.virtual-scroll-item.model-card)
- Add hamburger menu for mobile/small screens with proper translations
- Update all locale files with 'common.actions.menu' key
Fixes: Cards becoming too small/overlapping on narrow window widths (e.g., 1156px)
Changes: 15 files, +569/-114 lines
Handle models that are only available for on-site generation (usageControl:
"Generation" or "InternalGeneration") rather than downloadable.
Backend changes:
- Add usage_control field to ModelVersionRecord dataclass
- Extract usageControl from Civitai API responses
- Filter non-downloadable versions from update availability checks
- Add database schema migration for usage_control column
- Include usageControl in version response JSON
Frontend changes:
- Add isDownloadAllowed() helper function
- Show disabled download button for non-downloadable versions
- Add "On-Site Only" badge for restricted versions
- Update resolveUpdateAvailability() to filter non-downloadable versions
- Add CSS styling for disabled action button
Internationalization:
- Add translations for onSiteOnly badge and downloadNotAllowedTooltip
- Complete translations for all 10 supported languages
Detects when multiple model files share the same basename (causing
ambiguity in LoRA resolution), logs warnings during scanning, and
provides a "Resolve Conflicts" button in the Doctor panel. Resolution
renames duplicates with hash-prefixed unique filenames, migrates all
sidecar and preview files, and updates the cache and frontend scroller
in-place so the model modal immediately reflects the new filename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CivitAI returns file type "Diffusion Model" for checkpoint files (e.g., Anima
models), but the file selection logic only accepted "Model" and "Negative",
causing "No suitable file found in metadata" errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shouldBypassAutocompleteWidgetMigration only matched inputs by widget name,
but ComfyUI's migrateWidgetsValues also matches forceInput inputs (like "seed").
This discrepancy meant the bypass never triggered for TextLM/PromptLM nodes,
causing migrateWidgetsValues to filter out real widget values by incorrectly
mapping forceInput flags onto saved autocomplete values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_split _get_records_bulk into 500-id batches so the WHERE IN clause
never exceeds SQLite's 999-parameter ceiling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>