Commit Graph
1284 Commits
Author SHA1 Message Date
Will Miao 9daa9d7dd8 fix(workflows): remove copy filename from the card context menu 2026-10-31 08:31:39 +08:00
Will Miao e11c0e21e1 fix(ui): name each page's items instead of models in shared chrome
Shared chrome borrowed the model-library nouns on the Recipes and Workflows
pages:

- The header filter panel is titled per page (`recipes.filter.title`,
  `workflows.filter.title`) instead of "Filter Models".
- Bulk guards and move failures use page-specific copy
  (`recipes.bulk.noSelection`, `recipes.move.failed`,
  `workflows.bulk.noSelection`) instead of "No models selected" / "Failed to
  move model(s)", covering the bulk menu, the sidebar drag-and-drop path and
  the recipe move client.
- Workflows got a bulk-action config, so model-only bulk entries (send to
  workflow, auto-organize, re-import/rematch, copy syntax) stay hidden
  instead of appearing unfiltered.
- The workflows page's global context menu no longer offers the model-only
  example-image entries.
- MoveManager no longer assumes every API client exposes `modelType`: the
  recipe client has none, which made the "use default path" preview throw
  and stored the toggle under `use_default_path_undefined`.

Recipes card copy is translatable now too: the card tooltips (favorites,
share, send, delete, blur, NSFW levels) and the delete dialog go through
i18n instead of hard-coded English.

10 new keys (the move-only noSelection key folded into
`workflows.bulk.noSelection`), translated in all 9 locales; no
[TODO: Translate] placeholder remains.
2026-10-30 20:35:45 +08:00
Will Miao fed087eb8c fix(workflows): use workflow wording in card actions and dialogs
The workflow card menu reused the model-library copy ("Exclude Model",
"Delete Model", "Copy Model Filename"), and the shared delete dialog
hard-coded "Model:" in its info box.

- Workflow-specific labels for copy-filename / move / delete, and for the
  delete confirm dialog (title, message, info-box label, failure alert)
- Move dialog title, root label and hint now go through i18n instead of
  hard-coded English; bulk delete dialog switches to workflow copy
- Failed move / nothing-selected toasts use workflow wording
- "Copy Workflow Filename" now copies the file name; it used to click the
  card's copy icon, which copies the workflow document (duplicate of
  "Copy Workflow")
- Fix "Link to CivitAI" posting to /api/lm/loras/relink-civitai: the
  mixin's prefix map was keyed by singular type and missed the plural
  page type used by WorkflowContextMenu

Also drops the "Exclude Workflow" action: workflows, like recipes, have
no exclusion concept. The file has no consumer outside LM, and the
excluded view is only reachable from the global context menu, so
excluding a workflow was a one-way hide. The entry is removed from the
card menu, the global menu and the excluded-view banner; the backend
route stays available.
2026-10-30 20:10:45 +08:00
Will Miao 874d0e96d8 feat(workflows): single library folder + create from ComfyUI
- Replace the multi-root folder_paths.workflows workflow library with a
  scalar workflow_path setting (Settings > Library); remove the
  extra_folder_paths.workflows path and the standalone Model Paths editor.
- Migrate pre-release settings.json files that still keep
  folder_paths.workflows / extra_folder_paths.workflows on startup, so an
  upgrade keeps the configured folder instead of silently losing it.
- Add "Create LM-managed workflow" to the ComfyUI Graph menu and the File
  menu/command palette: it writes the serialized graph into the configured
  folder, stamps extra.lm_source and registers the entry in the scanner
  cache without forcing a rescan.
- Attach the decode image of the client's last executed prompt as the
  workflow preview. The prompt id is sent with the create request and the
  backend only honors it while it is still the registry's current prompt,
  so a tab never borrows another tab's output; otherwise the card keeps the
  placeholder preview.
- Detect library entries deleted/renamed while a graph still carries the
  marker: a link-status endpoint clears the stale extra.lm_source (on graph
  load, menu open and before create/update), and the write-back route
  returns 404 instead of recreating the document.
- Collapse the workflows.noPaths standalone/ComfyUI split into a single
  description/hint; the folder is edited in Settings > Library in both
  modes, so the manual settings.json guidance and the standalone
  open-settings-folder fallback are gone.
- Add the preview tensor conversion as a shared utility (reused by the
  recipe widget save path) and cover create, preview gating, link status,
  migration and menu injection with tests.
2026-10-30 17:35:53 +08:00
Will Miao 7adfa799e9 fix(ui): use the shared field radius in the phone header
The phone search field was forced to border-radius:999px. That looked
out of place next to the 6px used everywhere else, and at that radius the
1px border vanished along both arcs - the element's overflow:hidden
rounded clip cuts the outline, so the field read as a broken frame.

Use var(--border-radius-sm, 6px), i.e. exactly the wide-screen value.
Verified at 320/390/520/600 and focused, light and dark.
2026-10-29 21:03:57 +08:00
Will Miao 06e230ee39 fix(ui): keep a full search field in the phone header
The <=600px header collapsed the search to a floating 32px circle, which
left ~250px of empty bar at 600px and hid the primary action behind a tap
even though the width supported a real field.

- The phone breakpoint moves to 520px: at 521px the icon nav and the
  200px search floor still coexist, and 480-600px keep their tabs.
- Below that the search is an always-visible pill that fills the row and
  the current page name sits next to the logo, so the bar still says
  where you are now that the tabs live in the menu.
- Drops the click-to-expand overlay and its Escape handler: no fixed
  positioning, no transparent placeholder, no breakpoint duplicated in JS.
- Active menu entry uses an accent tint; the hover token it used before
  has no dark-mode value.

Verified 320-1920px: no clipping or overlap, page title appears at
381-520px and the menu carries all seven tabs.
2026-10-29 20:50:42 +08:00
Will Miao 735c313cc7 feat(ui): phone header - tabs in the menu, search in a button
Below 600px seven tabs plus a usable search field no longer fit. The bar
now reads logo | search | menu:

- The nav entries are repeated in the hamburger dropdown (shown there
  only at <=600px) and the main nav is hidden.
- The search collapses to a round magnifier and expands into a fixed
  overlay covering the header row on focus; Escape or a click elsewhere
  returns it to the button.
- SettingsManager.setNavEntryVisible keeps both copies of the opt-in
  entries (Other, Workflows) in sync when the switches are flipped.
- The dropdown scrolls when its 12 entries do not fit the viewport.

Verified in a headless browser from 320px to 1920px: no clipping and no
overlap, and the menu keeps its five actions above 600px.
2026-10-29 20:33:53 +08:00
Will Miao 96f957b38f fix(ui): keep the header search usable as tabs grow
The opt-in Other Models and Workflows pages took the nav to seven tabs
(~783px with icons), which left the header search field 73px wide at
1280px - just its two inline toggles. Rebuild the responsive ladder
around the measured widths:

- <=1500px: drop nav icons, keep the labels; icons return above 1500px
- <=1366px: compact gaps/padding and drop the Ctrl+F cue
- <=1150px: hide the app title
- <=1100px: collapse the five round controls into the hamburger
- <=780px: labels collapse to icons (was 700px; the row overflowed and
  clipped the hamburger by 39px at 720px)
- <=520px: drop the search field's 200px floor (the row was clipped by
  93px at 390px)

Also give the nav links the a11y metadata the icon-only states need:
aria-label (the label span is display:none below 780px), title, and
aria-current on the active tab; decorative icons are aria-hidden.
2026-10-29 20:24:49 +08:00
Will Miao 53f7ff407a fix(workflows): restore uses the app confirmation modal and a loading overlay
- Replace the browser confirm() with a generic #confirmationModal
  (template + ModalManager registration + showConfirmationModal() helper
  in modalUtils.js, promise-based, danger-styled confirm button).
- Show the loadingManager overlay while the restore request is in flight;
  .json restores re-download from CivitAI and the wait previously looked
  like a dead click. The overlay hides in a finally block.
2026-10-29 12:23:22 +08:00
Will Miao f473678004 feat(workflows): restore a workflow to its downloaded state
New card context menu entry 'Restore Downloaded Version' discards local
edits made via the write-back flow:

- Container artifacts (.zip/images) were never modified — restore deletes
  the derived .workflow.json sidecar and re-extracts from the pristine
  archive, then re-syncs analysis metadata. Purely local, works for
  self-hosted workflows too.
- Bare .json artifacts are rewritten in place by write-backs, so restore
  re-downloads the recorded CivitAI version file (API-key auth, temp file
  + workflow validation + SHA256 check before the atomic replace).

The menu entry is disabled for local-only .json workflows that have no
restore source. On success the card is refreshed in place with the
shared 'Updated' cue.
2026-10-29 12:12:52 +08:00
Will Miao e4369b0bd0 feat(workflows): show the Updated card cue after Update from ComfyUI
The write-back result carries the new document hash; the LM page now
refreshes the card in place via virtualScroller.updateSingleItem, which
re-renders it with the shared 'Updated' indicator.
2026-10-29 09:35:48 +08:00
Will Miao fa774de9ea feat(workflows): update managed workflows from the LM card context menu
Add an Update from ComfyUI entry to the workflow card context menu,
enabled only when the workflow is the active graph in a connected
ComfyUI tab and carries unsaved edits.

ComfyUI side (workflow_writeback.js): the first graphChanged after a
load/write-back flips a dirty flag inside the extra.lm_source marker
(ChangeTracker only dispatches on real state changes); the active
graph's link state (file_path, sha256, dirty) is reported to
POST /api/lm/workflows/comfyui-state on load, tab switch, dirty flip
and write-back. A new lm_collect_workflow WS handler answers LM-page
write-back requests: it reuses the regular PUT content flow and posts
the outcome to /api/lm/workflows/collect-result.

Backend: an in-memory WorkflowLinkRegistry holds per-client link states
(pruned against live websocket sids) and pending collect futures;
POST /api/lm/workflows/update-from-comfy targets the tab holding the
requested file (dirty holders win) and awaits the collect result,
mapping no_changes/cancelled to 200, timeout to 504.
2026-10-29 08:29:26 +08:00
Will Miao 2d9a245ea8 fix(workflows): label the modal button Open in ComfyUI like the other entry points
Workflows load a whole graph document rather than inserting into the
current graph, so all workflow entry points (card icon, context menu,
modal button) now consistently read Open in ComfyUI; model types that
insert into the current graph keep Send to ComfyUI.
2026-10-28 20:17:01 +08:00
Will Miao adc8713c0b feat(workflows): wire card icons and modal send button for workflows
The card send/copy icons and the model modal's Send to ComfyUI button
only had lora/checkpoint/embedding branches; workflows fell through to
a silent no-op (modal) or lora-ish behaviour with wrong tooltips (card).

- shared helpers sendWorkflowToComfy / copyWorkflowToClipboard in
  utils/workflowActions.js, reused by the context menu, card icons and
  the model modal
- card send icon opens the workflow in the running ComfyUI graph; the
  copy icon copies the workflow document JSON for paste-to-load
- correct tooltips (Open in ComfyUI / Copy workflow to clipboard)
- send entry points hidden in standalone mode (no graph to push into):
  card icon and modal button, matching the context menu
2026-10-28 20:09:10 +08:00
Will Miao 7efeb592fd feat(workflows): copy-to-clipboard flow and standalone-aware context menu
- GET /api/lm/workflows/content returns the extracted workflow document
  (json direct, zip/png/webp via the derived sidecar); no ComfyUI needed,
  so it also serves standalone mode where send-to-comfy is unavailable
- workflow cards gain a Copy Workflow context-menu action that puts the
  document JSON on the clipboard for paste-into-ComfyUI loading
- the Open in ComfyUI item is hidden in standalone mode, where pushing
  to a graph is impossible by definition
2026-10-28 19:19:36 +08:00
Will Miao 0c55dff828 feat(workflows): use WF as the workflow card badge abbreviation
The fallback produced WORK (first four letters), which reads like a verb
and is wider than the other badges; WF matches community shorthand and
the two-letter style of CN/CV/TE. Also give the workflow sub-type its
full display name so tooltips show Workflow instead of the raw key.
2026-10-28 17:43:59 +08:00
Will Miao 7221f5632d fix(download): show brand-new model cards without a manual refresh
The in-place reconcile from #1078 no-oped whenever the downloaded model
had no card in the current view, which silently swallowed brand-new
downloads (e.g. pasting a CivitAI URL for a model never downloaded
before): the backend added the entry but the listing only caught up on
manual refresh.

Reconcile now takes wasLocallyOwned (derived from the existsLocally
flags of the version list the modal fetched before the download):
- owned model outside the current view: keep the #1078 no-op
- brand-new or unknown ownership with no matching card: fall back to
  the legacy reload so the new card appears
2026-10-28 16:59:05 +08:00
Will Miao 720440507c feat: add opt-in workflow library management
Add a Workflows page that manages ComfyUI workflow JSON files as a
first-class library alongside loras/checkpoints/embeddings:

- opt-in via the new 'Workflow Management' settings toggle (default off)
- dedicated workflow folder roots with sidecar/preview support, separate
  from ComfyUI's own workflow storage; 'Send to ComfyUI' loads a workflow
  straight into the graph
- workflow scanner parses the graph document (UI/API format sniffing,
  node classes, model/civitai refs) and persists the analysis in the
  SQLite model cache
- settings UI: workflow folder paths editor in the Library tab and the
  standalone Model Paths page

Fixes from sandbox E2E verification:

- include the workflow scanner in startup initialization so the page no
  longer hangs on 'initializing' when enabled without folders
- add the missing workflows entry to the frontend page-state map (page
  init crashed and the sidebar fell back to the loras tree without it)
- project workflow analysis fields through the scanner cache entry and
  persist them in the model cache (new columns with auto-migration)
- keep symlinked paths intact in the directory picker (abspath instead
  of resolve), so stored library paths match what the user sees on disk

Also rebuilds the vue-widgets bundle to sync it with web/comfyui/utils.js.
2026-10-28 11:45:01 +08:00
Will Miao 493257bc3c feat(first-run): one-click bulk metadata fetch prompt after the first scan
A fresh install ends its first library scan with a page full of grey
placeholder cards. This adds the bridge from "scanned" to "illustrated
library":

- FirstRunManager.start_initial_scan arms a bulk_fetch_prompt_pending
  flag when the first scan actually launches. Upgrade shortcuts and the
  defer path skip it — those users already have metadata, or have no
  fresh scan to anchor the prompt to.
- New GET /api/lm/metadata-fetch-candidates reports how many cached
  models per page type qualify for a bulk fetch. It is read-only
  (in-memory cache only, never triggers a scan), excludes recipes, and
  shares the eligibility predicate with BulkMetadataRefreshUseCase
  (extracted as is_fetch_candidate/count_fetch_candidates), so the
  advertised number matches what a fetch would actually process.
- BannerService shows a one-shot banner with the candidate count and a
  Fetch Metadata action that runs the existing fetchCivitaiMetadata
  flow (progress bar + reload included). The flag is cleared once the
  banner displays, so it never nags twice; dismissal persists through
  the shared dismissed_banners setting.

Onboarding conflict is avoided structurally: the banner registers only
from the showPromos branch, which already requires the onboarding tour
to be finished or skipped and the first-run/init pages to be gone, so
the tour and this prompt never compete for the same moment.
2026-10-25 09:43:40 +08:00
Will Miao 8f9e7e54ad feat(init): structured byte-weighted scan progress broadcasts
The initialization page computed its ETA by regex-parsing the English
details strings ("Found N", "Processing x/y") and the progress bar
advanced per file, so one 7 GB LoRA and one 100 KB embedding moved it
by the same amount.

The scanner progress callbacks now carry the collected file sizes end
to end: process_models/count_models broadcasts include structured
processed/total/bytes_done/bytes_total fields, and the bar percentage
is weighted by bytes (falling back to file counts when sizes are
unavailable). initialization.js consumes the structured fields
directly - no more string parsing - and computes the remaining-time
estimate per byte, keeping the file-count path as a fallback.
2026-10-25 09:24:30 +08:00
Will Miao 86c0239b8c fix(init): honest error state and per-page completion for initialization
Two initialization-page defects made failures look like progress:

- A cache read exception while rendering a model/recipe page forced
  is_initializing=True, showing an endless spinner; worse, the polling
  fallback then reported the idle scanner as "complete", reloading the
  page straight back into the same exception. Page handlers now pass
  init_error to the initialization template, which renders the error
  state (with retry) server-side, and initialization.js stays on it
  instead of starting WebSocket/polling.

- The WebSocket path reloaded once the page's own scanner finished,
  while the polling fallback waited for every scanner. The polling
  fallback now mirrors the per-page semantics: /api/lm/init-status
  accepts ?page=<pageType> (aggregate behavior preserved without it),
  and ModelScanner records _init_error so a failed initial scan is
  reported as "error" instead of masquerading as "complete". Polling
  stops on error instead of reloading into the failure.
2026-10-25 09:02:57 +08:00
Will Miao 92820be4ad feat(first-run): gate the initial library scan behind a welcome page
Fresh installs no longer start a full library scan (with per-file SHA256
and sidecar writes) automatically at startup. Instead a welcome page
lists the configured model roots, estimates the scan cost from a quick
hash-free walk, and lets the user pick the sidecar storage layout and
UI language before confirming. Deferring shows an empty library; the
existing refresh button starts the scan later. Upgrading installs are
detected via a non-empty persisted cache snapshot (row-count check, so
the empty file created by scanner construction no longer masquerades
as an existing library) and keep the old behavior.

Bundled fixes surfaced by the new flow:

- broadcast an init-progress error when the startup scan fails instead
  of silently landing on an empty library; the initialization page now
  shows the error with a retry button
- skip the onboarding language modal/tutorial while the welcome or
  initialization page is up (its spotlight targets don't exist there,
  so the tour was silently auto-completed and never shown)
- promotional banners wait until onboarding is completed or skipped;
  alert banners (startup messages, cache health) are unaffected
- honor the hidden attribute in first-run/initialization CSS so the
  no-roots hint and error boxes don't render as empty frames
2026-10-25 09:02:57 +08:00
Will Miao 07fcaa137a fix(fetch): handle rate_limited status in metadata fetch UI
When a bulk metadata fetch hits a provider rate limit, the backend
aborts the batch and broadcasts {status: "rate_limited"} over the
fetch-progress WebSocket. The frontend switch in fetchCivitaiMetadata
never handled that status, so the completion promise never settled and
the loading overlay stayed up forever -- users had to cancel and
re-trigger, and the aborted batch silently never resumed.

- Add case 'rate_limited' to the fetch-progress switch: settle the
  promise so the overlay closes, and show an aborted status line.
- Show a warning toast with updated/failed/skipped counts and a hint to
  re-run Fetch from Civitai later.
- Reuse the failure-details modal with a rate-limited notice banner
  listing the models skipped due to rate limiting.

Backend behavior (abort after 3 consecutive rate limits, no automatic
resume) is the deliberate #1085 design and is unchanged.
2026-10-24 21:29:31 +08:00
Will Miao 0be3ea7d98 fix(download): accept mirror/www Civitai hosts and clarify linked-file errors
Follow-up to #1152:
- Accept civitai.green and www-prefixed hosts in isSupportedCivitaiPageHost,
  aligning with py/utils/civitai_utils.py and extensionReimportBridge.js, so
  download URLs on those hosts resolve too
- Drop the duplicate civitaiModelByVersion entry from the LoRA-specific
  endpoint map; the shared getApiEndpoints map now provides it for all types
- Report a distinct error when a linked fileId exists in the version but is
  not a downloadable model weight file (new linkedFileNotWeightFile locale key)
2026-10-21 21:53:59 +08:00
pixelpaws 8033292ad8 Merge pull request #1152 from ergs0204/fix/civitai-download-links
fix: accept Civitai download URLs and preserve file selection
2026-10-21 21:47:18 +08:00
ergs0204 b290cb8314 fix: preserve page version fallback and batch file availability 2026-10-21 19:39:45 +08:00
Will Miao 3c18dd6531 fix(delete): keep the cache in step when deleting duplicate models
Deleting a duplicate could remove the file from disk while leaving the
entry in the cache, so the duplicate kept showing after a refresh and
only Rebuild Cache cleared it (issue #1154). Three paths produced that
state:

* ModelHashIndex.remove_by_path() deleted _hash_to_path[hash_val] with no
  guard. The delete flow passes the cache row's sha256 as a hint; once
  the row and the index had drifted apart (a hash corrected in place, a
  partially applied cache update) the hint was not in the index and the
  KeyError aborted _batch_update_cache_for_deleted_models(), so every
  deleted model stayed in memory and in SQLite.
* The same cache update was skipped outright when the scanner's
  cancellation flag was set, turning files staged before the cancellation
  into ghost entries.
* A file that was already gone from disk could never be cleaned up:
  staging found no artifacts, the hard delete reported nothing, and the
  row survived as a permanent "No files deleted" failure.

Make the hash hint advisory and guard the index deletions; drop the
path-keyed entries (filename duplicates, autov2/autov3) even when no hash
mapping exists. Always apply the cache update after a delete, purge stale
index paths that never reached the cache, and treat a missing file under
a reachable folder as deleted while still keeping unreachable roots
offline. bulk_delete_models() reports success: false when nothing was
removed instead of presenting a no-op as a completed delete.

The duplicates UI now surfaces what it used to hide: per-file failures
from `results`, and a cache-update failure that needs Rebuild Cache.
2026-10-20 21:14:57 +08:00
Will Miao 90067de621 fix(move): keep drag-drop move alive when dragend clears the drag state
The #1148 fix made performDragMove() resolve the library root from
GET roots before reading the dragged paths. The browser dispatches
dragend right after drop, and handleCardDragEnd() calls
resetDragState(), so by the time the roots response arrived
draggedFilePaths was null and `this.draggedFilePaths.length` threw:

  TypeError: Cannot read properties of null (reading 'length')
  at SidebarManager.performDragMove (SidebarManager.js:711)

The throw happened before the try block, so no toast was shown and no
move request was ever sent -- drag-drop silently did nothing. The same
race hit the draggedRootPath fallback (aborting the move when the roots
request failed) and the draggedFromBulk cleanup (bulk mode stayed on
after a bulk move).

Snapshot filePaths / fromBulk / fallbackRoot before the first await and
use only the snapshot afterwards; _resolveMoveRoot takes the fallback
root as an argument instead of reading this.draggedRootPath.
2026-10-20 08:48:47 +08:00
Will Miao 6d9391672a fix(controls): keep model type dropdown hidden until it is usable
PageControls.syncExcludedViewState() un-hid #modelTypeDropdown whenever
the excluded view was inactive, racing the dropdown's async init. On
pages with a single sub-type (e.g. checkpoints with only "checkpoint"),
init returns early, leaving a bare caret trigger with no label and no
event handlers.

Let ModelTypeDropdown flag data-model-type-available after a successful
init and restore visibility from that flag, so single-type pages stay
hidden while excluded-view transitions still hide and restore an actual
dropdown. Add regression tests for both the single-type and excluded-view
paths.
2026-10-19 17:33:30 +08:00
ergs0204 a113b14f85 fix: support Civitai version download URLs with file IDs 2026-10-19 15:45:32 +08:00
Will Miao 965bfd33cd feat(controls): keep model counts readable in large libraries
Counts in the filter panel chips (base models, tags, model types), the All Types
dropdown and the Refresh scope rows were rendered as raw digits, so a library with
10k+ models turned every chip into a five digit pill that dominated the row it sat
in — and the Refresh menu grouped its numbers while the filter panel did not.

One formatter now renders all of them (`formatCount`): exact and locale-grouped
below 10,000, abbreviated to three significant digits above it, and abbreviated
only where the locale's Intl compact form is a real unit and narrower than the
plain number — German keeps 18.475 instead of the rounded 18.500, Russian keeps
18 475 instead of the wider 18,5 тыс.

Their hover text is always present and always exact ({name} — {count} models, and
... recipes on the Recipes page): an affordance that only exists above a threshold
is undiscoverable, and the tooltip is also what reveals a chip label that CSS has
ellipsized.

Chips gain a two-slot layout (the name ellipsizes, the count keeps a tabular
right-aligned slot) so a long tag name can no longer push the count out of the
chip; Refresh rows move from float:right to a flex row, which keeps the counts in
a right-hand column and lets a long root label ellipsize. The All Types group
tooltip moves onto its trigger so it stops leaking onto the menu options.

The two new strings ship as [TODO: Translate] placeholders in the nine locales;
the translation pass is the next commit.
2026-10-18 18:34:16 +08:00
Will Miao 6835880e0b style(controls): drop decorative icons from toolbar action buttons
Width pressure on the controls bar is real (see the max-width: 1500px
tightening in layout.css), and the icons on text-labeled action buttons
are redundant encoding. Remove them from Refresh / Fetch / Download /
Bulk / Duplicates / Doctor and the recipes-page Import / Batch Import.

Kept: state-carrying icons (favorites star, updates exclamation,
exit-duplicates fa-times, badges) and structural icons (carets, layout
toggle, dropdown menu items). ModelDuplicatesManager rebuilds the
duplicates button on mode exit, so its restore string is updated too.
2026-10-18 16:46:44 +08:00
Will Miao 3d65e044f8 feat(controls): add model sub-type quick-switch dropdown to the controls bar
Single-select Model Type dropdown next to the sort control on the loras,
checkpoints and other pages (hidden when only one sub-type exists).
Selection maps to filters.modelTypes, persists via the existing filter
storage, and stays in sync with the filter panel's multi-select chips.
2026-10-18 16:16:59 +08:00
Will Miao 6ab4b041c0 style(card): unify quant/hl badge height with version-name chip
Match font-size, line-height, and vertical padding of .quant-badge and
.hl-badge to .version-name across base/medium/compact densities so all
chips in .badge-version-unit share the same box height. Color remains
the sole semantic differentiator.
2026-10-18 11:58:41 +08:00
Will Miao f8ecc00f6b feat(workflow): user-controlled send-to-workflow target filtering
- Global exclusion lists (node classes + title patterns) in settings,
  applied at the shared filterRegistryNodes choke point; settings UI
  section is hidden in standalone mode
- Per-node "Exclude from Send Targets" context-menu toggle persisted
  in the workflow JSON (lm_exclude_send)
- Quick-exclude button on node selector items writes the comfy class
  to the exclusion list
- Precedence: per-node exclusion > explicit marker > global lists
- interceptModeChange now supports multiple listeners; registry hooks
  mode changes so mute/bypass refreshes candidates immediately
2026-10-18 09:42:44 +08:00
Will Miao 72705c570e fix(move): stop drag-and-drop from nesting the target under the source dir (#1148)
The sidebar drag-and-drop derived the library root by stripping the card's
cached folder off its file path. Any disagreement between the two -- a stale
or wrong-cased folder recorded by an older version -- made the strip fail and
silently returned the file's own directory as the root, so the move created
<current dir>/<target path> on disk and physically nested the library.

Two defenses:

* SidebarManager now resolves the move root from the authoritative library
  roots (longest prefix match) and only falls back to the card heuristic,
  which compares case-insensitively and aborts the move on a real mismatch
  instead of fabricating a root.
* Reconcile repairs existing entries whose folder no longer matches where
  their file_path sits. Refresh never rewrote existing rows, so folders
  recorded wrong by 1.2.2-1.2.5 kept poisoning moves forever; now a plain
  Refresh heals them (no manual Rebuild Cache needed).
2026-10-17 19:39:37 +08:00
Will Miao f940daae29 feat(sidebar): compact drag image, hover auto-expand, edge auto-scroll for card drags
Replace the browser's default full-card drag snapshot with a small chip
(thumbnail + name + count badge) anchored below-right of the cursor, so
the folder tree stays visible while aiming. Collapsed folders now
auto-expand after a short hover during a drag, and the tree auto-scrolls
when the pointer nears its top/bottom edge.
2026-10-17 18:20:42 +08:00
Will Miao 27275049b2 style(modals): redesign re-link and link-source dialogs
Compact single-field dialog layout: narrower content (640px), header with
accent icon and in-flow close button whose hover outline no longer crosses
the header divider, warning/info banner with theme-token side accents,
monospace URL example chips, and error text that only takes space when
present. Buttons switch to the standard secondary/primary pair.
2026-10-16 16:43:19 +08:00
Will Miao 19abf7ed03 feat(ui): show quantization badge on model cards
Distinguish same-version quantized models (e.g. GGUF Q4_K_M vs Q8_0) on
cards, which previously showed only the identical CivitAI version name.

Detection is content-based and stored as a three-state sidecar field
(absent = unchecked, "" = checked-unavailable, value = canonical name),
mirroring the autov3 pipeline:

- GGUF: minimal pure-struct header parser reads general.file_type
- safetensors: dominant tensor dtype F8_* -> FP8
- CivitAI files[].metadata.fp == "fp8" fills in as a fallback only
- A one-time backfill service covers existing libraries; each file
  costs only a few-hundred-byte header read

The footer renders the badge after the version name (suppressed in
grouped mode), gated by a new show_quantization_on_card setting that
follows the show_version_on_card toggle pattern.
2026-10-15 11:18:06 +08:00
Will Miao e27500c80f Merge branch 'feat/download-root-rules'
Download Root Rules: route downloads to a non-default root by ordered
base-model/tag rules per model type (loras/checkpoints/unet/embeddings),
with a rules editor in Settings > Organization, a default-root cue, and
the matched rule shown in the download modal. Translated in all locales.

Conflict note: settings_manager.py adopted the shared
utils.root_paths.normalize_root_identity from main (issue #1146).
2026-10-13 09:23:35 +08:00
Will Miao b68af5400e fix(sidebar): shorten the missing-folder dialog copy (#1145)
The dialog sits in the sidebar and now offers the removal itself, so telling the
user the entries "can still be removed from the sidebar" said the obvious twice
and read oddly next to a button that does exactly that. Say what is true and stop:
"This folder no longer exists on disk. You can remove it from the list."

The result toast follows the button's wording ("removed from the list") instead of
naming the sidebar again. Both strings changed in English, so the existing
translations of them are back in the queue with the new source text rather than
left saying "refresh the sidebar and try again".
2026-10-12 10:35:04 +08:00
Will Miao 3a718ce2c3 fix(sidebar): let a ghost folder be cleared from the UI (#1145)
Deleting a folder a file manager had already moved away answered "Folder no longer
exists" and stopped there: `remove_known_folder()` only ran after `shutil.rmtree()`
succeeded, so the records keeping the node in the tree could never be dropped, and
the modal's missing state hid its confirm button. The sidebar was a dead end for
exactly the case the folder list cannot self-heal.

* `ModelMoveService.forget_folder()` drops a folder's records (the recorded entry
  plus the cached models under it) and refuses with `code: "exists"` while any root
  still holds the directory, because then the caller wants a delete.
* `delete_folder()` forgets the records instead of failing when the directory is
  already gone. The dry run stays a pure read and keeps answering `missing`, which
  is what makes the dialog offer the removal. `POST`/`GET
  /api/lm/{prefix}/forget-folder` is the endpoint for a node that maps to no
  directory at all (the GET form is for the browser extension, which is GET-only).
* The sidebar falls back to it from the delete flow, offers "Remove from list" in
  the missing state, and reports the folder as removed from the sidebar rather than
  as a delete that never happened. An `exists` conflict says the directory is back
  and should be deleted instead.

Verified: 3811 backend tests, 1543 JS tests, 96 Vue tests, 20 i18n tests.
2026-10-12 09:31:29 +08:00
Will Miao d5139500f9 fix(settings): stop clipping help tooltips in the settings pane
Help tooltips were CSS pseudo-elements on the info icon, so they were
descendants of the scrollable settings pane (.settings-content uses
overflow-y: auto) and got clipped at its top edge. The Active Library
deprecation notice made this obvious: the text grew to ~10 lines while
only ~5 fit above the icon, so the tooltip started mid-sentence at
"page. Deprecated:".

Render the tooltip once into <body> with position: fixed instead, so no
overflow ancestor can clip it, and position it from JS: above the trigger
when it fits, below otherwise, clamped to the viewport. It follows the
trigger on scroll/resize and hides on mouseout, Escape or touch. The
delegated listeners also cover help icons rendered later by the settings
manager, and the box may now use 320px instead of 220px, which keeps long
help texts to fewer lines.
2026-10-11 11:34:52 +08:00
Will Miao cbbefdecf9 fix(theme): make checkboxes and loading spinner follow the theme accent
Native checkboxes in settings (managed types, base-model skip list) and the
folder-root delete modal had no color declaration, so checked state used the
browser default blue; the loading spinner used hardcoded #3498db/#f3f3f3.
Give the checkboxes accent-color: var(--lora-accent) and derive the spinner
track/arc from the accent token so all themes and presets stay consistent.
2026-10-10 20:51:19 +08:00
Will Miao c0165919b3 fix(theme): readable on-accent text color and unified accent aliases
Solid-accent buttons used the page text color (--lora-text) or white,
which falls below WCAG AA on the mid-bright accent (L 52-72%) in every
theme/preset — e.g. ~1.7:1 in dark+solarized, making primary buttons
hard to read. Add --color-accent-contrast (dark, low-chroma, same hue,
derived per preset) aliased as --text-on-accent, and switch ~90
accent-background selectors (primary/confirm buttons, nav items, chips,
badges, checkmarks) to it.

Also define the previously dangling --lora-accent-l/c/h, --lora-accent-hover
and --lora-accent-light aliases (fixing ~40 dead declarations), replace the
hardcoded rgba(--lora-accent-rgb, 79 70 229) fallbacks with oklch channel
syntax, and add the missing margin above .template-custom-input in
settings.
2026-10-10 20:41:10 +08:00
Will Miao 3d165f92e0 fix(theme): use opaque surface tokens for modals and sticky layers
The elevated surface tokens (--bg-elevated / --surface-elevated) are
translucent, so sticky modal footers and table headers double-composited
over the modal surface and showed a visible lighter seam in light themes.

Add derived opaque tokens (--bg-elevated-solid / --surface-elevated-solid,
aliased as --lora-surface-solid) and use them for .modal-content, the
download/import modal sticky action footers, and the refresh result
table's sticky header, so the modal surface and its sticky layers always
share one opaque color across every theme and preset.
2026-10-10 19:58:54 +08:00
Will Miao a8941f453f fix(sidecars): use button-triggered wording in the migration dialog
The "Migrate Sidecars Now" button reused the mode-change confirmation
copy, which told the user they could migrate later with the very button
they had just clicked. Add manual variants of the confirm messages
(confirmToCentralizedManual / confirmToAlongsideManual) that drop the
mode-change preamble and the circular backreference, and translate them
for all 9 locales.
2026-10-10 18:58:56 +08:00
Will Miao 5796a0e2e9 fix(sidecars): report over-long migration paths and fix root relocation (#1142)
- Surface Windows WinError 206 / ENAMETOOLONG during sidecar migration as
  an actionable per-model error instead of a bare OSError
- Offer root relocation based on the resolved sidecar root, so moving off
  (or back to) the default storage directory relocates existing sidecars
- Hint in the migration confirm dialog when the destination is the default
  location, so a custom folder can be set before migrating
2026-10-10 14:35:08 +08:00
Will Miao 377a1337db feat(recipes): show a has-workflow badge on recipe cards 2026-10-10 09:02:48 +08:00
Will Miao 587d952604 feat(recipes): add 'Has Workflow' filter to the recipes filter panel 2026-10-10 08:33:54 +08:00