Other Models management is now opt-in: enable_other_models (default false)
plus the enabled_other_sub_types allow-list replace the unreleased additive
enabled_other_folders key.
- config._get_enabled_other_folder_keys() is the single scan gate; a new
refresh_other_roots() rebuilds roots and preview roots on toggle.
- ModelScanner gains a _should_keep_cached_entry() hydration hook and
on_library_changed(reconcile=...) so switching a sub_type off drops its
entries (and hash/autov3 rows) at load time and switching it on rescans.
- OtherScanner filters location-derived entries accordingly.
- Other routes reject every other type while off (or a disabled sub_type) and
expose an "other_disabled" page flag; download routing returns a disabled
marker instead of guessing; the download manager refuses other-type
downloads and default-path routing for switched-off sub_types.
- Doctor / init-status / refresh-all skip the other scanner while off; the
scanner stays registered so staged pending-deletes still merge.
- Tests updated with explicit opt-in fixtures plus new gating coverage.
The example now only carries use_portable_settings, civitai_api_key and the
four core folder_paths keys (loras/checkpoints/unet/embeddings). Optional keys
such as the other-model folders, default_*_root and auto_organize_exclusions
are removed; their defaults live in DEFAULT_SETTINGS and reach the user's
settings.json on demand.
AGENTS.md now forbids adding optional/default keys to the example unless the
user explicitly asks for it.
- Drop the fixed max-width on .header-container so the header spans the
viewport while the card grid keeps its own content width
- Keep header icon click targets at 32px at all breakpoints and add
role/tabindex/aria-label plus Enter/Space activation
- Relocate the Doctor trigger from the page toolbar to the header icon
group (also available on the statistics page and in the hamburger
menu), removing the now-unused .doctor-trigger styles
The include_empty folder tree (download/move modals) walked every model
root synchronously on the event loop via get_all_folders(). On network
(NAS) roots this froze the whole server for the duration of the walk —
blocking WebSocket progress, aria2 RPC and the download queue — and the
5s TTL re-triggered the walk on nearly every modal interaction.
The scanners already visit every directory during cache scans, so record
the full directory list (including empty folders) there instead:
- _gather_model_data/_reconcile_cache collect directories during the
existing walks; reconcile refreshes and persists the list even when no
model files changed.
- ModelCache gains an all_folders field (None = never recorded).
- PersistentModelCache stores the list in a new folders table, with a
cache_meta flag distinguishing 'recorded empty' from legacy snapshots.
- get_all_folders() is now a pure in-memory read. A legacy snapshot
triggers a one-shot backfill walk in a worker thread (never on the
event loop) that records and persists the list.
- Moves add the destination folder (and parents) incrementally instead
of invalidating a TTL cache.
The browse endpoint and its frontend were written with POSIX-only
assumptions, so on Windows pressing Browse immediately failed with
"Access denied to this directory":
- The frontend opened the browser at "/", which resolves to the
current drive root on Windows.
- The allowlist check used Path("/"), which has no drive letter on
Windows, so relative_to() rejected every drive-qualified path —
anything outside the user profile was denied.
Fixes:
- Empty browse path now defaults to the user home directory instead of
erroring; the frontend sends "" rather than the POSIX-only "/".
- The access check is platform-aware (drive-qualified on Windows,
absolute on POSIX).
- Parent navigation uses the server-provided parent_path; the root
check is now path.parent == path (the old str/anchor comparison
self-looped at Windows drive roots).
- Browsing up from a Windows drive root shows a virtual list of
available drives so users can switch drives without typing a path.
The Windows-only case-insensitive match in ModelScanner._reconcile_cache
is the only pass left unverified by the recent realpath cleanup: realpath
may already cover case differences on Windows, and if the branch is ever
reachable it is O(files x cache entries). Records the reachability
question, the verification steps for a Windows run, and the two possible
fixes.
A no-change Refresh still computed os.path.realpath for every model file
in the library and for every cached entry. Both values are only ever
consulted when a discovered file is missing from the cache, so on a
50k-file library they cost ~1.3s and ~0.6s while being used zero times.
- Compute the per-file realpath only after the exact cache match fails
- Build the physical-path alias map lazily on the first miss; the
cross-run alias guard (overlapping roots / symlink layout changes)
still keeps the cached entry instead of a delete + re-add, which would
re-read metadata and re-hash the whole library
- Snapshot get_model_roots() once for the new-file pass instead of
re-reading it for every added file
- Run the duplicate-path integrity pass only when the snapshot already
contained duplicates or files were appended; a clean, unchanged cache
has nothing to clean. Duplicates can only be introduced by external
code rewriting raw_data or by this pass's own appends.
Zero-change reconcile drops from ~1400ms to ~120ms on 50k files, and an
alias flip still re-processes 0 files (#1108 investigation).
The download modal's location step decided between checkpoint and unet
roots using only the CivitAI file-type signal, while the backend also
falls back to DIFFUSION_MODEL_BASE_MODELS. Models like Anima (file type
"Model") were offered checkpoint roots in the UI even though
use_default_paths would route them to the unet root.
- Extract the two-tier decision into py/services/download_routing.py and
reuse it in DownloadManager._execute_download
- Add POST /api/lm/download/routing so the UI asks the backend for the
routing decision; fall back to the local file-type check on failure
- ModelVersionsTab: search both checkpoint and unet roots when resolving
an existing version's download path
A cancel landing between aria2.addUri acceptance and the _transfers
registration found no tracked transfer, so DownloadManager tolerated the
"not found" and only cancelled the asyncio task — the daemon kept
downloading the file untracked while history showed the download as
cancelled.
- Register the gid in _transfers immediately after addUri returns,
before any further await (state-store persist moved after it)
- Shield the addUri RPC so a mid-flight cancellation still learns the
accepted gid and forceRemoves it before re-raising CancelledError
- On cancellation during the state persist, remove the daemon transfer
unless it is paused (skip_download relies on paused gids surviving)
The option only existed in the LoRA page menu. Move updateEnrichMenuItem
and enrichWithAgent into ModelContextMenuMixin so both pages share the
implementation, and add the menu item to the checkpoints template.
Moving a checkpoint into a unet root (or vice versa) moved the file and
updated the in-memory cache, but three stale spots survived until a
manual cache rebuild:
- The moved .metadata.json kept the old sub_type, and the opportunistic
sync_cache_from_metadata path (fired by get_model_metadata and example
image metadata updates) trusted it, reverting the cache entry and the
SQLite snapshot to the pre-move sub_type. Loader nodes filter strictly
on sub_type, so the model stayed listed under the old type.
- The manager page discarded the move response's cache_entry, so the
card badge (CKPT/DM) and context menu label kept showing the old type.
Fixes:
- move_model now re-resolves sub_type from the target location (new
resolve_sub_type_for_path hook) and persists it into the moved
.metadata.json.
- _sync_cache_from_metadata_impl runs desired entries through
adjust_cached_entry so location-derived fields cannot be re-poisoned
by stale metadata snapshots.
- MoveManager carries cache_entry.sub_type into the in-place card
update so badge and context menu reflect the new type immediately.
detectUrlType only matched /resolve/ links, so pasting a HF web preview
(/blob/) URL fell through to direct-http and surfaced a misleading
'Invalid CivitAI URL format' error. Treat blob URLs as hf-resolve.
Replace the post-run toast cascade and the standalone L4 results modal
with a summary modal modeled on the batch download summary: 3-state
header, stat cards (matched / needs review / unresolved / errors),
an L4 review table with per-entry undo, and a copyable report. Wired
into the global, bulk and single-recipe rematch entries; complete
no-op runs keep the lightweight toast. Obsolete results-modal code,
styles and i18n keys are removed.
- Snapshot pre-rematch entry state (reconnectSnapshot) so rematched
entries can be undone via the existing restore flow
- Bulk missing-LoRA downloads mark unresolvable failures hash-invalid,
flipping those entries from download to reconnect candidacy
- Recipe modal always offers a reconnect action next to download for
missing LoRA entries
- Rematch runs collect an opt-in relaxed-matching choice (also reconnect
missing models by file name) via a pre-run options dialog on the
global, bulk and single-recipe entries
- L4 (filename-level) matches are listed in a results dialog with
per-entry undo
The .onboarding-target-highlight class sets position: relative, which
overrode .folder-sidebar's position: fixed (equal specificity, later
stylesheet). The sidebar left fixed positioning and moved in-flow, while
the spotlight/mask cutout stayed at the pre-highlight rect, leaving an
empty highlighted region during the folder sidebar step.
Whitespace cleanup in cleanupLoraSyntax() and the autocomplete blur
formatter collapsed all whitespace runs, including inside <lora:...>
tags. A file named 'test - 0021.safetensors' was rewritten to
'test - 0021' in the node text, so runtime file resolution failed.
Protect lora tags with placeholders (or segment splitting) so only
whitespace between entries is normalized; names inside tags are kept
byte-for-byte.
The field dates back to a development-stage bug in the enrich-metadata
(agent) pipeline, which briefly wrote trigger words at the top level of
model metadata instead of the established civitai.trainedWords location.
The write path was fixed before the feature merged to main (PR #1013)
and never shipped in any release, so no writer has existed since.
Remove the leftover pieces:
- BaseModelMetadata.trainedWords field (py/utils/models.py); sidecars
from that dev window now pass the key through _unknown_fields instead
- HF download handler's strip-empty-trainedWords special case, reverting
to saving the metadata object directly (py/routes/handlers/hf_handlers.py)
- trainedWords in the LLM enrichment context (agent_service.py)
- matching fallbacks/fixtures in the enrich_hf_validation harness and
post-processor test
Trigger words continue to live in civitai.trainedWords for all model
sources, which is what the UI, agent post-processor, and metadata sync
all read and write.
models.dev is served by Cloudflare with brotli compression when the
client advertises it, and brotli is a required dependency here, so
aiohttp always negotiates br. A corrupted br stream can crash the
native decoder with a Windows access violation (a Python-level
exception handler cannot catch it), or produce garbage bytes.
Send an explicit "Accept-Encoding: gzip, deflate" header on the model
catalog and Ollama model-list requests so the server never returns
brotli. zlib handles corrupt gzip data by raising ContentEncodingError
(an aiohttp.ClientError subclass), which the existing handlers already
catch and degrade to a warning with an empty-catalog fallback.
The public REST API rewrites files[].name to "{model}_{version}" for
non-LoRA model types, so every precision variant of a multi-file version
shared one name and landed on disk with a random short-hash suffix.
Fetch the raw stored filename from the model-versions/mini endpoint
(always pinned with modelFileId) and use it for the on-disk name and
metadata when available; fall back silently to the REST name otherwise.
CivArchive already serves raw names and is skipped.
Recipes imported from CivitAI image URLs can contain 0 LoRAs: the backend
only sees the REST image API + EXIF, while the complete generation data
lives in the image page's internal trpc payload (see
docs/recipe-civitai-image-no-metadata.md). When the companion
lm-civitai-extension is installed with a valid license, re-import (single
and bulk) of CivitAI-image-sourced recipes is now delegated to the
extension via DOM CustomEvents; the extension scrapes the image page with
the user's session and calls back into the reimport endpoint with the
full metadata payload. Without the extension (or with an invalid license)
the native path runs unchanged.
- POST /api/lm/recipe/{id}/reimport accepts optional payload params
(image_url/name/resources/gen_params/base_model/tags); the payload path
reuses the import-remote engine with reimport semantics (user-edit
carryover, delete-after-save), and malformed/failed payloads fall back
to the legacy URL import. Response gains loras_count.
- The endpoint also accepts GET: the extension is GET-only by convention
(documented in AGENTS.md).
- New static/js/utils/extensionReimportBridge.js (probeExtension /
delegateReimport / getCivitaiImageInfo) wired into RecipeContextMenu
and BulkManager with silent native fallback.
- i18n: toast.recipes.reimportingViaExtension added and translated in
all 9 locales.
Page-imported recipes can carry an exact CivitAI modelVersionId but no
modelId and no hash (CivitAI exposes no sha256 for e.g. Krea versions).
canDownloadLora() required (modelId && versionId) or a hash, so such
entries were misclassified as unrepairable and offered Reconnect instead
of Download.
- canDownloadLora: treat a bare version id as downloadable (it uniquely
pins the file; the model id is resolved on demand at download time).
A model id without an exact version id stays non-downloadable to avoid
silently grabbing the latest version.
- resolveLoraDownloadIdentifiers: when a hash is absent but a version id
exists, resolve the owning model id via /civitai/model/version/{id}
(same endpoint the bulk download missing flow uses). Hash-only and
direct (modelId+versionId) paths are unchanged.
resp.json() raises UnicodeDecodeError (not JSONDecodeError) when the
remote body contains invalid UTF-8 bytes, which the exception handler
did not catch and could crash the app. Apply the same fix to both
_load_model_catalog and fetch_ollama_models so they fall back to an
empty catalog. Add regression tests for both paths.
The recipe "Repair Metadata" action has been marked Deprecated in the UI
for a while and cannot reliably recover recipes imported from CivitAI URLs
whose REST meta has no resources/hashes and whose image has no embedded
metadata (e.g. CivitAI-only generation data). Drop the feature end to end.
Backend:
- remove repair routes (repair, cancel-repair, recipe/{id}/repair,
repair-bulk, repair-progress) and their handler mappings/methods
- remove RecipeScanner repair_all_recipes / repair_recipe_by_id /
_repair_single_recipe and REPAIR_VERSION
- remove WebSocketManager recipe-repair progress channel
- drop repair_version column from the persistent recipe cache
- rematch mutual-exclusion now only checks rematch
Frontend:
- remove repair entries from per-recipe, bulk and global context menus
- remove repairRecipe / repairSelectedRecipes / repairRecipes + cancelRepair
and the repairBulk API client method/endpoint
- drop recipe-repair i18n keys (synced across locales; doctor keys kept)
Tests/docs: delete test_recipe_repair.py, update scaffolding/routes/ws/
persistent-cache/integration tests and i18n guideline examples.
Commit 86aa1d80 added align-items: flex-end to .toast-container and
dropped the .toast min-width to 200px. With flex-end alignment each
toast now shrinks to its own content width, so toasts of different
message lengths render at inconsistent widths. Drop the align-items
override so the container falls back to stretch, giving every toast a
single shared width as before the change.
Adds a 'Keep Action Bar Visible' toggle (default off) under
Settings > Interface > Layout Settings. When enabled, the controls bar
(Refresh, Download, etc.) and the breadcrumb nav are wrapped in a shared
sticky container (.sticky-topbar) so both stay pinned as one unit; when
disabled, the wrapper is display: contents and the original behavior
(only the breadcrumb stays visible) is preserved.
Rebuilt from the preceding three commits' sources:
- active-filters chip and its dead settings-toggled broadcast removed
(the built bundle also no longer embeds the scripts/app.js test shim
that the chip's settings.js import used to pull in)
- scrollbar inset re-measured on programmatic value changes
- app/api bindings canonicalized to "../../../scripts/*" externals and
settings.js no longer inlined (bound at runtime via "../settings.js"
to the vanilla module instance), per the new build guard
The loramanager.lora_active_filters_autocomplete tooltip only mentioned
the /activefilters and /noactivefilters commands, while the prompt-node
tag-autocomplete tooltip cross-links every toggle entry point (typing in
the node and the node's right-click menu). 6ba64ebb added the right-click
menu entry without extending the tooltip to match; align the wording with
the established pattern.
The --lm-vscrollbar-width inset from 634ea7f2 was only refreshed on input
events, mount and mode changes. Programmatic value updates (widget.setValue
from "send lora to workflow", external value-change events) change the
textarea content without an input event, leaving the corner clear (x)
button overlapping a freshly appeared classic scrollbar until the next
keystroke. Mount-time pending value replay was already covered.
- onExternalValueChange and widget.onSetValue now call
updateVScrollbarWidth() alongside the hasText update
- tests: cover both paths by overriding textarea metrics to an overflowing
state and asserting the 15px gutter lands in the CSS var
Guard against the inlined-shim bug class that broke the removed
active-filters chip: importing web/comfyui/* modules from widget source
inlines them into lora-manager-widgets.js, and their own relative imports
then resolve against the repo filesystem at build time instead of the
vanilla files' runtime URL layout.
A resolveId plugin (enforce: pre) now returns explicit external markers:
- scripts/app.js and scripts/api.js imported at any "../../scripts/*"
depth are rewritten to the canonical "../../../scripts/*" specifier so
every app/api binding in the bundle is the real ComfyUI module. The
repo-root scripts/app.js is a unit-test shim (in-memory settings store)
and must never be bundled; the canonical depth is the only one that
resolves from the emitted bundle's served location.
- web/comfyui/settings.js is externalized to "../settings.js" so the
bundle binds to the SAME vanilla module instance the ComfyUI extension
loader already runs - real settings store, registerExtension side
effect executed exactly once, no duplicated module state.
A companion plugin warns on any web/comfyui/* import from widget source,
since an inlined copy still duplicates module-level side effects.
Notes from validating the mechanism: rollup output.paths resolves
returned paths to absolute filesystem locations (rejected), and a depth
regex inside rollupOptions.external matches raw specifiers before
resolveId hooks run and would emit the shim-relative depth verbatim
(rejected) - hence explicit { id, external: true } returns.
The indicator chip added for /activefilters discoverability was broken by
design of its import path: AutocompleteTextWidget.vue imported
web/comfyui/settings.js into the vue-widgets bundle, and settings.js's
"../../scripts/app.js" import resolved at build time to the repo-root test
shim (scripts/app.js, an in-memory settings store). The chip therefore read
and wrote an orphaned in-memory Map: clicking it flipped only its own
visual state and never touched the real ComfyUI setting that
autocomplete.js consults (use_active_filters query param).
Beyond the defect, a persistent per-node control for a global persisted
setting misleads users and needs cross-instance sync machinery, which the
footer hint, slash commands, right-click menu entry and settings dialog
already cover.
- AutocompleteTextWidget.vue: remove the chip button, its state/handlers,
the settings.js import (the shim-inlining pathway) and all chip styles
- AutocompleteTextWidget.test.ts: drop the chip indicator describe block
and the settings.js module mock; beforeEach import no longer needed
- settings.js: drop the lora-manager:setting-toggled window broadcast and
its export — the chip was its only consumer, so every
setLoraManagerSettingValue write no longer dispatches a dead event
- autocomplete.activeFilters.test.js: drop the broadcast assertion test
- loraLoader.activeFiltersMenu.test.js: drop SETTING_TOGGLED_EVENT_NAME
from the settings.js mock
Discoverability of /activefilters // /noactivefilters is unchanged:
command-list footer, first-run hint, node context menu, settings dialog.
The absolutely-positioned clear (x) and active-filters filter buttons sit at
the textarea's right edge, so when content overflows and a classic
(non-overlay) vertical scrollbar appears the buttons overlap it. Measure the
scrollbar gutter (offsetWidth - clientWidth) when content overflows and
expose it as --lm-vscrollbar-width on .input-wrapper; the buttons' right is
now calc(base + var) so they shift left of the scrollbar only while one is
present (0 otherwise, incl. overlay-scrollbar platforms).
Refreshed on input, mount, canvas/Vue-DOM mode change and via a ResizeObserver
on the textarea (widget resize). Rebuilt the vue-widgets bundle.
Mirror the /noautocomplete discoverability pattern for the loras\nactive-filters search toggle:\n\n- autocomplete.js: extend the slash-command-list footer and the\n one-time first-run hint to loras nodes, advertising\n /activefilters and /noactivefilters\n- lora_loader.js: add an 'Active Filters Search: ON/OFF' entry to the\n right-click menu of all loras-autocomplete node classes\n- settings.js: broadcast a 'lora-manager:setting-toggled' window event\n on every setLoraManagerSettingValue write\n- AutocompleteTextWidget.vue: add a persistent filter indicator chip\n (loras mode only) that reflects and toggles the setting and stays in\n sync via the setting-toggled event\n- tests: footer/hint/event coverage, context-menu tests for all four\n node classes, widget indicator tests; rebuild vue-widgets bundle
- Replace timestamp comparison (help_last_viewed vs a hardcoded date) with
a content-version marker (data-help-content-version) read from the
rendered modal markup, so badge state always reflects the content
actually served
- Only mark content as viewed when the modal is opened while it contains
new content; opening a stale pre-upgrade page no longer suppresses the
badge after a refresh
- Flag the Replay Tutorial button itself with a 'New' chip (hidden by
default, one-time glow animation) and scroll it into view when
revealed; tab-level dots now mark getting-started and shortcuts
instead of documentation
- Translate help.newContentBadge into all 9 locales, reusing the
established help.documentation.newBadge renderings
- Add HelpManager content-version unit tests (12 cases)
- Fill all 41 [TODO: Translate] placeholders per locale (new onboarding
steps, Shortcuts cheat-sheet tab, trigger-word copy/edit tooltip)
- Retranslate stale onboarding bulk/contextMenu step contents to match
the updated en.json source
- Follows docs/i18n-translation-guidelines.md term maps, register, and
punctuation rules; HTML tags and key names preserved verbatim
- Bind R=refresh, F=fetch metadata, D=download in PageControls via
eventManager (plain letters only, skipped while typing or when a
modal is open); triggers reuse the buttons' existing click handlers
- Show key-hint chips on the refresh/fetch/download/bulk toolbar
buttons; convert the bulk chip to a semantic <kbd>
- Redesign shortcut hints as a neutral theme-adaptive keycap:
--shortcut-* variables in base.css now derive from --text-muted
with a bottom-edge shadow, shared by the toolbar chips, the header
search cue, the help-modal cheat sheet, and onboarding key hints
- Add shared isTypingContext() helper to uiHelpers
- Add an Actions group (R/F/D) to the Shortcuts cheat-sheet tab
Verified with vitest (926 passing, incl. 6 new shortcut cases) and a
sandboxed E2E run in real Chrome (light/dark rendering, hover state,
'?' opening the Shortcuts tab, clean console)