Auto-select the first (newest) version for URLs without an explicit
modelVersionId, matching the existing batch flow, so users can proceed
to location/download without manually picking a version.
os.path.commonpath raises ValueError for paths on different Windows
drives. Treat that as no common root so cross-drive recipes migrations
succeed instead of failing with 'Invalid recipes path change'.
Move the 'Download Missing' / 'Re-process All' example image actions
under a single 'Download Example Images' submenu item in the single-model
and bulk context menus, matching the existing send-to-workflow submenu
pattern. Shorten the submenu labels and update all locale translations.
Split the single-model and bulk context menu actions into 'Download
Missing Example Images' (regular endpoint, skips already-processed
models) and 'Re-process Example Images' (force endpoint, retries
failed models).
- start_download accepts model_hashes so a selected subset can be
processed with the progress-aware skip logic; explicitly targeted
models bypass the failed/processed model-level guards so per-image
gaps are filled
- pre-download existence check in the processor skips network requests
for image files already on disk across all download paths
- force download retries previously failed models and clears their
failed status on success
- add i18n keys for the new menu items across all locales
The model field now accepts either a manual string or a MODEL connection.
When wired, the model name is extracted from the patcher's
cached_patcher_init (registered by core loaders load_checkpoint_guess_config
and load_diffusion_model, preserved through LoRA clones) and converted to a
ComfyUI-style relative name via config model roots.
- model input declared as "STRING,MODEL" with widgetType STRING, so the
text widget and the dual-type connection slot coexist; non-STRING/MODEL
links are rejected by frontend and backend type validation
- UNETLoaderLM GGUF branch now registers a custom cached_patcher_init reload
factory so GGUF models participate in name extraction and ModelPatcher
deepclone/dynamic machinery
- shared collect_overwrite_params() helper keeps the node and the metadata
extractor conversion logic in sync; extraction failures are logged instead
of silently dropping the overwrite
After b464fdc3 (preserve .git on release switch), the hasGit-based
channel detection is unreliable — .git now exists for both release
and nightly installs, so page refresh always reset the channel.
- Add _resolveChannelFromSettings() with migration heuristic:
!hasGit → release (ZIP), detached HEAD → release (on tag),
on branch → nightly. Uses gitInfo.branch from check-updates.
- Persist resolved channel to settings.json on first load
(one-time migration) and on explicit switchChannel.
- Add update_channel validation (release|nightly) in backend
update_settings handler.
- Remove hasGit-based guessing from initialize(); defer to
checkForUpdates where full gitInfo is available.
- Channel resolution runs before checkForUpdates early-returns
to avoid null channelMode on reload-within-interval.
Tests: 361 passed.
Move reverse-migration logic from get_model_folder() (hot path, called on
every metadata/example-images request) to ExampleImagesMigration, where it
runs once at startup. On network storage this was causing 22-38s delays
per LoRA card click.
Additionally optimize prune_stale_example_images() to read the directory
listing once instead of per image entry (O(N*M) → O(M)). Also reorder
consolidation checks so regex filters run before filesystem stat calls.
Previously, switching to the release channel would delete .git/ and
fall back to a ZIP download. This broke update.bat, manual git
commands, and CM git-based update detection.
Now the release path uses git checkout <latest-tag> when .git exists,
and only falls back to ZIP when .git is absent (CM CNR installs).
.git is never deleted - the ZIP→nightly path remains a one-way
upgrade via _init_git_repo.
Also updates locale strings (en, zh-CN, zh-TW, ja) to remove the
now-inaccurate "remove the Git repository" wording.
switch_channel has three destructive code paths (git reset + clean,
git init + checkout --force, and rmtree + ZIP replace) that were
missing the _stage_preserved_items / _restore_preserved_items safety
net already applied to perform_update.
Wrap the channel-specific logic in a try/finally so preserved user
data (settings.json, civitai/, cache/, etc.) is physically moved
outside plugin_root before any git operation and always restored.
Move settings.json, civitai/, wildcards/, backups/, stats/, logs/,
cache/, and model_cache/ to a temp directory before git reset/clean
or ZIP replacement, then restore them in a try/finally block.
This prevents data loss on Windows where git clean -e exclusion
patterns can fail due to path-separator mismatches or where file
locks (open SQLite/log handles) cause the restore step to be skipped
on failure.
Also unifies three hardcoded skip lists (_clean_plugin_folder,
skip_items, skip_tracked) to derive from the single _PRESERVE_DIRS
constant, fixing drift where logs/ was missing from the ZIP path.
The compare API URL format compare/{local_hash}...main returns
status='ahead' when main is ahead of the local commit. The count is
in the ahead_by field, not behind_by. The old code only read behind_by
which is always 0 in this case, causing the UI to show 'Up to date'
when actually several commits behind.
Also handle status='diverged' (both sides have unique commits) by
reading ahead_by for the remote-ahead count.
Frontend adds a hash comparison fallback: if behind_by is 0 but local
and remote commit hashes differ, show 'Behind main' instead of the
incorrect 'Up to date'.
Tests: _AheadCompareDownloader and _DivergedCompareDownloader mocks
for the two status paths.
When downloading a diffusion model (UNet) from the checkpoints page, the
download modal's location step always showed checkpoint roots and paths.
Now the modal detects the file subtype and switches to unet_roots endpoint,
default_unet_root key, and 'unet' path template.
- Add POST /api/lm/switch-channel endpoint with git init / ZIP fallback
- Add _backup_git/_restore_git helpers with safe rollback
- Version-info endpoint now returns has_git flag for auto-detection
- Check-updates always returns releases (changelog) regardless of channel
- Nightly mode shows 'N commits behind main' with commit hash and date
- View on GitHub link points to /commits/main in nightly mode
- Channel toggle UI with pill-style buttons in update modal
- Confirmation dialog with Esc / backdrop-dismiss support
- Channel derived from has_git on every page load, no localStorage
- i18n: 11 new keys translated across 9 non-English locales
- CSS: unified card-style sections in _base.css
- Tests: 8 new tests covering switch-channel, nightly response, init_git_repo
Two bugs prevented type-signature-based fallback from working:
- metadata_hook.py used getattr(obj.__class__, 'RETURN_TYPES')
which fails when _async_map_node_over_list is called with
a class (not instance) — obj.__class__ is the metaclass
'type', which has no RETURN_TYPES. Fixed: getattr(obj, ...).
- metadata_registry.py used type(extractor) is GenericNodeExtractor
to dispatch return_types. NODE_EXTRACTORS stores class
references, not instances; type(Class) is always 'type',
never the class. Fixed: extractor is GenericNodeExtractor.
GenericNodeExtractor (previously a no-op) now inspects
RETURN_TYPES to detect MODEL loaders and CONDITIONING
encoders in nodes not registered in NODE_EXTRACTORS.
- Propagate return_types from the hook layer through the
registry to GenericNodeExtractor.extract() and update().
- MODEL detection: scan ckpt_name/unet_name/model_path/
model_name/gguf_name fields, validate by extension.
- CONDITIONING detection: scan text/clip_l/t5xxl/prompt
fields, store prompt text and conditioning tensor.
- _fill_missing_metadata also checks node_cache, so
GenericNodeExtractor-handled nodes survive cache.
Users can now right-click nodes and assign meta hints
(primary_model, primary_sampler, positive_prompt,
negative_prompt) to override the metadata processor's
heuristic inference.
- Store extra_data from the API request so workflow node
properties (including lm_marker_role) are accessible
during metadata processing.
- _get_user_marks scans extra_data.extra_pnginfo.workflow
for meta_* marks, falling back to prompt.original_prompt.
- extract_generation_params checks user marks before
heuristic inference for sampler, model, and prompts.
- Warn on duplicate marks or invalid marked nodes.
Drop the SequenceMatcher-based fuzzy_match fallback that froze the server
when FTS returned empty results. FTS now returns empty set for zero results
(no fallback), and when the index is not yet ready, search returns empty
rather than scanning all items in Python.
The previous tooltip was misleading: users thought workflow embedding was
automatic. New wording explains this opt-in flag stores the complete
workflow inside images, allowing one-click restoration via drag-and-drop.
PNG and WebP only.
Add two new optional parameters to the Save Image node:
- webp_method (INT, 0-6, default 6): Controls WebP compression level.
0=fastest/largest, 6=slowest/smallest. Previously hardcoded to 0.
- jpeg_subsampling (INT, 0-2, default 0): Controls JPEG chroma
subsampling. 0=4:4:4 (best quality), 1=4:2:2, 2=4:2:0.
Frontend JS extension hides/disables each parameter when the
selected file_format doesn't apply (e.g., webp_method is hidden
when saving as PNG or JPEG). 7 new tests cover parameter plumbing
and default consistency across INPUT_TYPES, save_images(), and
process_image().
hydrate_model_data replaces model_data with .metadata.json content which
may lack sha256 (corrupted file, concurrent write, etc.). Restore the
cached sha256 after hydration and persist the fix back to disk so
subsequent lookups don't hit the same error.
Also improve error log to include file_path for debugging.
- Replace plain-text Lora hashes with Hashes JSON dict matching A1111 convention
- Add Civitai resources JSON array with AIR URNs for direct model version linking
- Add Clip skip, Version: ComfyUI fields to generation params line
- Build AIR strings from local scanner cache (no API calls needed)
- Add complete sampler name mapping (CIVITAI_SAMPLER_MAP) and base model → AIR slug mapping (BASE_MODEL_AIR_SLUG) sourced from civitai ecosystem constants
- Remove lora text prepending from prompt line; LoRA info now in structured JSON sections
LM Studio and some other OpenAI-compatible servers reject
response_format=json_object but accept json_schema. Switch to the
equivalent json_schema format and add a fallback that retries
without response_format when the provider rejects the format type.
widget.value is a getter/setter that returns a new array on every read,
so handleStrengthDrag with updateWidget=false mutated a discarded copy.
Introduce __dragActive flag to suppress renderLoras in setValue during
drag, allowing mutations to persist through widget.value without
destroying the DOM. Use try-finally to guarantee flag cleanup.
The validation in getApiEndpoints threw for page types not in
MODEL_TYPES (e.g. 'recipes'), crashing the recipes page initialization
when FilterManager calls it via createBaseModelTags(). The throw was
synchronous and outside the fetch().catch() chain, causing an uncaught
promise rejection that aborted the entire app initialization.
getApiEndpoints is a URL builder -- validation belongs to callers that
need strict type checking (they already use isValidModelType()). For
non-model-type pages like recipes, the generated URLs are correct
(the backend does have /api/lm/recipes/* routes).
Fixes regression from f53f859a (feat(filter): add debounced tag search).
- Move preset apply handler from span.preset-name to div.filter-preset so
clicking anywhere on the tile triggers the preset, not just the label text.
- Add whitespace heuristic in showToast() to skip translate() for plain
messages that are already translated at the call site. This prevents
i18next from logging 'Translation key not found' for pre-translated
strings like 'Preset "name" applied'.
- Use safe .get() in RecipeCache._resort_locked instead of itemgetter to prevent KeyError when recipe missing created_date; align sort key with _sort_cache_sync (prefer modified, fallback created_date, fallback 0)
- Add base_model to allowed_fields in persistence_service.update_recipe() so the field passes validation
- Route bulk base model updates through updateRecipeMetadata() on recipes page instead of generic saveModelMetadata(), matching existing isRecipesPage pattern used in setBulkFavorites and saveBulkTags
The drop indicator top position was calculated using only
getBoundingClientRect() offsets (post-CSS-transform viewport space)
without accounting for container.scrollTop (pre-transform layout space).
This caused the indicator to drift upward as the user scrolled down,
eventually disappearing entirely.
Fixed by adding container.scrollTop to the position calculation and
only dividing the GBCR visual-diff portion by scale, since scrollTop
is already in pre-transform coordinate space.
- Add enable_civarchive_api toggle (default on) to allow disabling
CivArchive to avoid its rate-limit windows entirely
- Add metadata_provider_order dropdown with two presets:
CivitAI → CivArchive → Archive DB (default) and
CivitAI → Archive DB → CivArchive
- Wire both settings through backend (metadata_service, settings_manager,
misc_handlers) and frontend (SettingsManager, state, settings modal)
- Reorder Metadata section in settings modal: toggles → status/management
→ fallback order, for natural top-down workflow
- Make update_metadata_providers() log the effective provider chain
using actually-registered providers rather than settings assumptions
- Add 5 test cases covering all provider-combination paths
- Complete i18n translations for 6 new keys across all 9 non-English locales
- Add PersistentModelCache.update_single_model() for lightweight targeted
SQL update (single row + incremental tag/hash deltas, no full table scan)
- Add ModelScanner.sync_cache_from_metadata() with compare-first logic:
skips entirely when cache is already in sync; when stale, updates the
entry in-place (O(1) instead of O(n) remove+append), incrementally
adjusts tag counts/hash index/version index, and resorts only when
sort-relevant fields changed
- Wire sync_cache_from_metadata() into BaseModelService.get_model_metadata()
via fire-and-forget asyncio.create_task — disk I/O is already paid for
- Include identity re-validation guard against concurrent cache replacement
- Add 16 tests covering _cache_entries_differ, sync_cache_from_metadata
(no-change, in-place, fallback, conditional resort), and
update_single_model (insert, tag delta, hash delta)
When a KSampler marked as 'Send Gen Params Target' has widget inputs
wired to Primitive nodes (PrimitiveNode, PrimitiveInt, PrimitiveFloat,
etc.), sending gen params from the Lora Manager UI now updates the
Primitive node's value instead of the KSampler widget. This is
necessary because ComfyUI's execution engine reads from the connected
input, ignoring the widget value when a wire is present.
Also fix two minor issues found during review:
- Remove unnecessary String() wrapping on numeric gen params (seed,
steps, cfg) to preserve native types through the JSON/WS path
- Correct misleading isNodeEnabled comment: LGraphEventMode values
are 0=Always, 2=Never, 4=Bypass (not 'Normal/Enabled')
When Civitai returns 404 for /models/{id} (e.g. due to Civitai API bug
where un-deleted models still get 404), the fallback to CivArchive
provides metadata. However CivArchive may return mirrors with every
entry marked deletedAt, while the file's downloadUrl is still valid.
Before this fix, _build_download_urls_from_file_info used an if/else
that skipped the downloadUrl fallback whenever the mirrors array was
non-empty, even when all mirrors were filtered out. Now downloadUrl
is always tried when no usable mirror remains.
Also deduplicated the inline mirror-processing code at the second call
site by replacing it with a call to the shared helper.
Refactor _create_session() to make-before-break: snapshot old session,
assign new one first, then close old. Previously, concurrent download
retries called _create_session() without the session lock (violating its
docstring contract) and closed the old session while other coroutines
held active references — causing aiohttp to raise "NoneType has no
attribute connect" when dereferencing the torn-down connector.
Also wrap the two _create_session() calls in the integrity-retry and
network-retry paths with self._session_lock to match the locking
discipline used by the session property and refresh_session().
- Add Notes/Description tab switching with tab state persistence in widget value
- Lazy-load model description and version description from /lm/loras/metadata
- Render CivitAI HTML descriptions inline via v-html
- Auto-fetch description when LoRA selection changes while on Description tab
- Fix Vue mode height containment via contain:layout size (lm-vue-node class)
- Fix scroll wheel isolation: widget scroll vs canvas zoom in both render modes
- Add docs/comfyui-dual-mode-widgets.md with widget rendering patterns
In ComfyUI Vue render mode, WidgetDOM.vue reuses its component instance
during undo/redo without re-calling mountWidgetElement(), leaving newly
created widget containers detached from the DOM.
- AutocompleteTextWidget: scan for empty containers by ID prefix and reuse
- Loras widget: scan for empty .lm-loras-container elements and reuse
- Prevent duplicate event listeners by guarding listener setup on new
containers only
- Keep container in DOM on cleanup (clearChildren instead of remove)
so it can be found and reused by the next factory invocation
When using full path lora syntax, the context menu (single/bulk)
and bulk copy actions were passing only the file basename to
buildLoraSyntax(), ignoring the folder prefix. This caused the
output to look like legacy A1111 format even when full path mode
was enabled.
Aligns all entry points with ModelCard.handleSendToWorkflow(),
which correctly includes the folder prefix.
Also fixes selectAllVisibleModels() to cache the folder field,
preventing missing prefix on select-all-then-send flows.
Add GET variants of the two POST endpoints used by the send-to-workflow
feature. Parameters are read from query string instead of JSON body,
supporting both simple repeated node_id params and JSON-encoded node_ids
for complex graph references.
In add_to_queue, check download_history before INSERT OR IGNORE. Without
this check, a fire-and-forget /queue/complete failure on the extension side
would allow the same download_id to be re-inserted after complete_download()
deleted it from the queue — creating phantom queued entries for already-
finished downloads.