344 Commits
Author SHA1 Message Date
Will Miao f5bbb64a61 Add third-party LoRA node adapter API 2026-11-13 10:35:27 +08:00
Will Miao d81218f3f0 feat(settings): link directly to ComfyUI node settings 2026-11-11 08:25:09 +08:00
Will MiaoandAlexandre Zia 196e3f6c22 fix(widgets): enable listed LoRAs in replace mode
- Shift+click "Send to ComfyUI" sends mode=replace, but mergeLoras keeps
  the saved active flag of entries already in the list, so a listed but
  disabled LoRA stayed off after the textarea was replaced.
- Call the shared enableListedLoras() after the merge, as the append path
  does, so replace turns the target on and keeps its saved strength.
- Cover the path with the real mergeLoras in the send-to-comfy tests.

Based on the replace-mode coverage in willmiao/ComfyUI-Lora-Manager#1096.

Co-authored-by: Alexandre Zia <11000327+alexandrezia@users.noreply.github.com>
2026-11-09 17:44:12 +08:00
Will Miao e2f4e349da fix(widgets): keep the autocomplete text widget gap constant in canvas mode
In legacy canvas rendering the search box and the LoRAs list sat flush on a
fresh node, and their gap grew while the node was resized. The canvas branch
of applyAutocompleteTextLayoutFix tried to clear the custom sizing with
`delete`, but computeLayoutSize() lives on DOMWidgetImpl.prototype, so the
delete was a no-op. The text widget stayed growable (60-100px) and shared the
node's free space with the LoRAs widget, while its 60px min-height overflowed
the DOM widget's 10px bottom margin: the visible gap was
min(20, allocation - 60).

Shadow computeLayoutSize with an own undefined property and give the widget a
fixed computeSize. Adjacent ComfyUI DOM widgets normally keep a 2 * margin
(20px) gap, which is too wide for the search box + LoRA list pair, so
allocate elementHeight + 8px instead: the search box stays 60px and the gap
stays constant at any node height, matching Vue node mode.

Also document the overlay margin math and the prototype-shadowing constraint
in the dual-mode widget guide.
2026-11-07 22:29:08 +08:00
Will Miaoandsmhc 8269cc7b66 feat(autocomplete): turn on listed LoRAs when picked from the dropdown
The "already added" cue was a single check, so a listed-but-disabled
LoRA looked identical to an enabled one, and picking either inserted a
duplicate tag that left the disabled entry off.

Show two states — a green "Added" pill and an amber "Off" pill — and
make the pick act on them: an added LoRA is enabled in place (reusing
the shared add-or-enable helper), an active one is left alone, and the
typed query is removed either way so no duplicate tag or stray search
text is left in the prompt.

Co-authored-by: smhc <6404304+smhc@users.noreply.github.com>
2026-11-07 20:20:17 +08:00
Will Miaoandsmhc f0a8bb85c8 feat(autocomplete): match LoRA titles when the node shows model names
Node rows can show library titles (Settings > LoRA Manager > Node LoRA
Name Display), but the textarea autocomplete only ever searched file
names. In "Model Name" mode a title fragment that does not appear in the
file name therefore returned nothing, even though the LoRA was visible
on the node.

Title matching follows the existing label setting instead of adding a
new toggle: File Name mode (the default) sends the unchanged request and
gets the same results and ordering as before. Only Model Name mode asks
the endpoint for title matches, which are appended after every filename
match so a path search is never reordered or hidden. The dropdown then
shows the title as the primary row label with the file name as secondary
text, matching what the node rows display.

Also cue results that are already on the node. The set is read from the
node's LoRA list (falling back to the textarea tags), normalized like the
backend resolves names, so legacy bare-name entries are recognized too.

Co-authored-by: smhc <6404304+smhc@users.noreply.github.com>
2026-11-07 20:06:23 +08:00
Will Miaoandsmhc 638675449d fix(widgets): enable listed LoRAs when sending to ComfyUI
"Send to ComfyUI" appended a tag for a LoRA the node already listed,
which left a disabled entry disabled and made the button look broken.
Route the append path through shared add-or-enable helpers: listed LoRAs
are switched on in place (strengths kept), unlisted ones still append.

Reused from willmiao/ComfyUI-Lora-Manager#1181.

Co-authored-by: smhc <6404304+smhc@users.noreply.github.com>
2026-11-07 20:06:20 +08:00
Will Miao 81148015c5 fix(widgets): keep node labels during refresh and trim cycler payload
- Send version_name only for titles shared by several files and guard
  non-dict civitai values, so the cycler payload stays small.
- Keep cached labels after the availability TTL expires instead of
  flashing file names while a refresh is in flight.
- List every covered node in the setting tooltip.
- Cover the payload rules and the stale-cache labels with tests.
2026-11-03 21:25:17 +08:00
smhc 61742a222c feat(widgets): label node LoRA rows with the library model name (#1175) 2026-11-03 21:21:03 +08:00
Will Miao 7eec0c1ec8 fix(randomizer): execute displayed loras after a manual re-roll
The Always Randomize / Generate Fixed / Reuse Last buttons replace the
displayed loras without touching the seed chain, so the next queue's
beforeQueued shifted the stale seeds and the backend regenerated the
previous seeded roll, silently discarding what the widget displayed.
Reset the seed chain on manual rolls so the next execution takes the
first-execution path and runs the displayed loras.
2026-11-02 14:21:53 +08:00
Will Miao fe33518221 fix(widgets): coalesce trigger-word syncs while the graph loads
ComfyUI creates every node before configuring any of them, so while a Lora
Loader restores its `loras` widget the upstream cycler/randomizer already
exists but still holds its factory-default config. The resulting sync
resolved an empty LoRA set, cleared the connected TriggerWord Toggle, and the
upstream node's own configure repopulated it a moment later. A draft or
change-tracker snapshot taken in that window keeps the cleared state as the
baseline, so the repopulated - and correct - graph compares dirty.

Coalesce syncs per source node and send only the last computed set. The
pending timer is stored on the node because utils.js is loaded twice (directly
by lora_loader.js and inlined into the Vue widget bundle), so both call sites
have to share it.

Reproduced on Lora_Cycler: the tracker baseline held
`toggle_trigger_words: []` / `orinalMessage: ""` while the live graph matched
the saved file byte for byte.
2026-11-01 08:13:19 +08:00
Will Miao e7e09b7ac9 fix(widgets): keep pool/cycler refreshes out of the saved widget values
The pool and cycler widgets recompute derived state shortly after a workflow
loads (preview count, current cycle index/name). Writing that back made the
serialized node differ from the saved JSON, so ComfyUI's ChangeTracker marked
the untouched workflow modified on the next mouseup - the comparison only runs
on canvas interaction, which is why it looked like clicking dirtied it.

- Lora Pool: serialize `preview` only when the filter selection changed; the
  live preview (match count) still refreshes on mount and on filter edits.
- Lora Cycler: the mount refresh and the first pool-config resolution (the
  link only becomes resolvable once the graph is configured) update the UI
  without rewriting the saved config. User edits and queue time still sync it.

Covered by the useLoraPoolState, useLoraCyclerState and LoraCyclerWidget
load-time tests.
2026-11-01 08:13:04 +08:00
Will Miao 14e90006de fix(previews): detect video previews from the versioned preview URL
Preview URLs now carry a cache-busting `&v=<mtime_ns>` suffix, so the media
type could no longer be read from the end of the string. Model cards,
duplicates, recipe cards and modals, the showcase thumbnail and the ComfyUI
preview tooltip all fell into the image branch and rendered `<img>` for video
previews, which browsers cannot decode.

Move the query-aware video detection that already lived in the versions tab
into a shared mediaTypes helper and use it everywhere a server-provided
preview URL is inspected. The web UI and the ComfyUI widget bundle each keep a
copy because they are served from different mounts. Video previews that turn
out to be webm now also get the right `<source type>`.

Add regression coverage for versioned mp4/webm/image URLs.

Fixes #1168
2026-10-31 16:46:04 +08:00
Will Miao 5b44b662d9 fix(widgets): restyle the selected LoRA's trigger-word highlight
The chips marked as coming from the selected LoRA were outlined with
--lm-warning-border, an amber at 55% alpha. Composited over the Light
palette's #C9C9C9 that lands at 1.09:1, so the highlight practically vanished,
and amber already means "warning" elsewhere (out-of-range strength, locked
LoRA). The translucent ring also sat on the chip border with the black shadow
behind it, reading as a fuzzy third edge.

Replace it with an offset ring: a 1px gap in --lm-surface, then a 2px ring
mixed from --lm-accent and --lm-fg. Both tokens follow the palette, so the
ring stays at 5.6:1 on Light and 9.0:1 on Dark, over both an active (accent)
and an inactive chip, and the gap matches the container background in the
canvas and Vue node renderers. The ring extends only 3px, so the existing 6px
gap and 1px margins already leave room; no layout or JS changes.

Re-declare the ring on hover: .comfy-tag:hover has the same specificity and
comes later, so hovering used to replace the whole box-shadow and drop the
highlight.
2026-10-31 12:39:08 +08:00
Will Miao ee08a863a4 fix(prompt): keep trigger word slots aligned when a workflow loads
Since frontend 1.16 the loader rebuilds a node's inputs from the node
definition - which now inserts `seed` - and then repairs every link target by
slot name. The prompt extension renamed `trigger_words` and appended a slot
during that same configure pass, so `filtered_trigger_words` could no longer be
matched by name and kept its saved target slot, which by then addressed the
`text` widget input (the textarea disappeared and the link was drawn into it).
Dynamic slots also drifted out of order and numbering.

Skip every slot mutation while the graph configures, keep `localized_name` in
sync when renumbering (the label prefers it over `name`), and normalize legacy
layouts once the graph has loaded: connected slots first, exactly one trailing
empty slot, numbered 1..n. Nodes already in the canonical layout are left
untouched, so loading does not mark workflows dirty.

The backend now accepts the bare `trigger_words` name used by workflows saved
before the current definition. The Lora_Randomizer and Lora_Cycler templates
are migrated to the canonical layout.
2026-10-31 12:05:15 +08:00
Will Miao c712a079a9 fix(widgets): stop load-time widget rewrites from marking workflows dirty
TriggerWord Toggle serialized the transient `highlighted` UI state into
widgets_values, but highlight is recomputed after load from the current
selection/library, so an untouched workflow always compared dirty against
the saved JSON and ComfyUI prompted to save on close. Strip `highlighted`
from serialized tag values (backend never reads it).

Also restore saved widget values by widget name instead of positional
index (position drift corrupted unrelated widget slots on load), and
preserve `locked` in mergeLoras so load-time merging no longer drops it.

Workflows saved with `highlighted` keys will show as modified once after
upgrading; saving them once writes the clean format.

Fixes #893
2026-10-31 10:34:39 +08:00
Will Miao 92de2dbc52 feat(workflows): add Create/Update to the tab context menu
ComfyUI exposes commands and menuCommands for the menu bar, but the
workflow tab context menu is built inside WorkflowTab.vue and has no
extension hook. Reuse the registered create/update actions and inject
the entries into the reka-ui portal, the same fallback the Graph menu
already uses.

The entries sit above the tab-close group. Clicking one activates the
right-clicked tab through app.extensionManager.workflow.openWorkflow
(the switch the native Save/Export entries use), so the action targets
that tab's graph instead of whichever tab was active. Update is enabled
from that tab's own lm_source marker and link-status check, and a menu
instance is only enhanced once.
2026-10-30 18:21:16 +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 eebdfcb0b6 fix(widgets): theme ComfyUI node widgets from palette tokens
LM widgets were painted with a fixed dark palette: translucent slate fills
plus light text. On the dark palettes those values happened to blend into the
node, but on the Light palette the translucency composited into grey slabs and
light text landed on light surfaces. The prompt textarea was the clearest
case: it used the fixed --color-charcoal-400 scale token, which never flips
with the theme, so it stayed charcoal inside a white node.

Add web/comfyui/lm_theme.css as the single colour layer. Every value aliases the
active ComfyUI palette (--comfy-input-bg, --component-node-*, --comfy-menu-bg,
...) and derives raised/hover/selected surfaces with color-mix, so built-in and
user-imported palettes are covered without per-theme rules. Contexts select the
token family the neighbouring built-in widget uses: lm-widget-root for canvas
DOM widgets, lm-vue-node for the Vue node renderer, lm-popover for menus
appended to <body>. Only status colours, shadows and scrims keep a light/dark
variant. Whole-node containers no longer paint a background, so the node
surface shows through and only inputs, cards and buttons are filled.

lm_styles.css, the canvas widget JS (autocomplete dropdown, tag chips, group
editor) and all Vue SFC styles now consume these tokens; tag category badges
derive their hue from oklch tokens so the tint and text contrast flip with the
theme. Empty states get an explicit 12px size: the canvas DOM overlay inherits
the 16px body size while Vue nodes render at text-xs, which made the
placeholders oversized and heavy in canvas mode.

A regression test rejects new colour literals outside the preview-image
overlays, and the DOM widget guide documents the token map and the
blend-don't-overlay rule. Rebuilt the committed vue-widgets bundle.
2026-10-30 09:48:29 +08:00
Will Miao e1f3d28107 Merge branch 'feature/workflow-management' 2026-10-29 16:44:08 +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 ee3edd5643 feat(workflows): write back ComfyUI graph edits to managed workflows
Open in ComfyUI now stamps an extra.lm_source provenance marker
(file_path + document sha256) onto the loaded graph, which rides the
workflow draft serialization across tabs and reloads. A new
LoraManager.UpdateManagedWorkflow command (palette, menubar File group,
and an item injected into the workflow actions dropdown; disabled unless
the active graph carries the marker) serializes the current graph and
PUTs it to /api/lm/workflows/content.

The write targets the workflow document: bare .json artifacts are
rewritten in place, while zip/image containers keep their original bytes
(the SHA256 is their CivitAI identity) and receive the edited graph in
the derived .workflow.json sidecar. Optimistic concurrency via
base_sha256 rejects writes with 409 when the library copy changed since
open (force retries after user confirmation). Post-write, analysis
fields and (for .json artifacts) hash/size/modified are refreshed in the
metadata sidecar and caches.
2026-10-28 21:17:51 +08:00
Will Miao 5b0fdc8f17 feat(lora-compat): auto-remap legacy Anima LoRA block indices
Anima 2.9B/3.8B insert blocks between the original 28, so old LoRA keys land on the wrong layers and ComfyUI emits no missing-key warning. Add a model-family adapter that rewrites block indices in memory before the generic LoRA loader runs.

- 28->40, 40->52 and composed 28->52 mappings, with anchored kohya/generic key patterns
- Anima detection from image_model/llm_adapter and block count from blocks/num_blocks
- skip text encoder, llm_adapter and semantic connector keys; collision and newer-LoRA errors
- ComfyUI setting (default on) synced to the LM settings API; no base-model-specific node toggle
- docs, unit and loader integration tests

Reuses the manifest data from PR #1104; supersedes its node-level toggle design.
2026-10-28 20:00:20 +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 1f58e78717 fix(loaders): migrate backslash values on subgraph boundary widgets (#1149)
Promoted combo widgets on a subgraph boundary hold their own store-backed
copy of the interior widget's value, seeded from the workflow's serialized
widgets_values, so migrating the interior loader widget never reached them:
the boundary combo kept the old backslash spelling, the frontend flagged it
as a missing model on load, and the error entry (keyed to the host node)
survived the interior node's onWidgetChanged.

- migrateWidgetValue generalizes the separator migration to any widget.
- applyToAllNodes now also migrates the projected boundary widgets of every
  subgraph node; their value setter writes through to the frontend's widget
  store, and firing the host node's onWidgetChanged clears the host-keyed
  error entries. Fixing the values before the post-load missing-model scan
  means the flags are never raised at all.
- The per-node onConfigure hook schedules a boundary sweep via
  queueMicrotask so pasted subgraph nodes (which bypass the graph-level
  configure hook) get covered once the paste settles.
2026-10-23 08:46:12 +08:00
Will Miao a19c1056dd fix(save-image): resolve %date% at save time and never overwrite counter-less files
The frontend serializeValue hook resolved %date:...% at queue time, baking
the queue timestamp into the serialized prompt. Prompts queued within the
same second (or re-run from history) shared one filename and silently
overwrote each other when add_counter_to_filename was off. %date% now
passes through to the backend, which formats it with datetime.now() at
save time; the date: check also runs before the '.' split so formats with
dots (e.g. %date:yyyy.M.d%) are not mistaken for %Node.Widget% patterns.

As a safety net, counter-less saves now fall back to an incrementing
suffix when the target file already exists instead of overwriting it.
2026-10-22 06:55:58 +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 1276673e0e fix(loaders): migrate backslash values in subgraphs and clear the error flag (#1149)
The follow-up report found three gaps in the value migration:

* applyToAllNodes only walked the root graph, so loader nodes inside
  subgraphs kept their backslash values. It now recurses through
  node.subgraph (Subgraph extends LGraph, so .nodes works at every level).
  Promoted combo widgets share the interior widget's store-backed value,
  so migrating the interior widget fixes the subgraph boundary too.
* Pasted nodes never hit the graph-level configure hook. A per-node
  onConfigure wrapper now runs the same sanitize + migrate pass after
  widget values restore.
* The corrected value kept its stale "value not in list" flag. The
  migration now fires the node's onWidgetChanged hook, which the
  frontend's error-clearing chain uses to drop value_not_in_list errors.
2026-10-17 17:12:41 +08:00
Will Miao be0133a699 fix(loaders): accept backslash-spelled model names from old workflows (#1149)
The forward-slash paths fix changed the checkpoint/unet combo lists on
Windows from "dir\model.safetensors" to "dir/model.safetensors", so
workflows saved before it loaded with a value no option matches: the
front-end extension reset the widget to the first model, or ComfyUI
flagged it "value not in list" when the widget was wired.

Two layers:

* random_loader_control.js migrates the widget value to its
  separator-normalized form when that form is a known model, before the
  reset-to-first fallback and before the wired early return.
* The loader nodes declare VALIDATE_INPUTS for the model name input,
  lifting ComfyUI's strict list check for it; the shared helper resolves
  separator-normalized names against the cache, so API-submitted or
  programmatic values validate too. get_checkpoint_info_absolute now
  normalizes backslashes explicitly instead of via os.sep, which was a
  no-op on Linux.
2026-10-17 10:19:53 +08:00
Will Miao 42fa8294df fix(widgets): hide DOM widgets from Properties Panel to avoid WidgetLegacy width pollution
The frontend's right-side Properties Panel falls back to WidgetLegacy for
unregistered widget types, and WidgetLegacy.draw() writes widget.width
(≈panel width) onto the real widget object. The canvas DOM overlay honors
widget.width ?? node.width, so clicking a node with the panel open squashes
the widget content to the left until undo/recreate (ComfyUI_frontend #11574).

Setting hideInPanel: true on all addDOMWidget options keeps LM widgets out
of the panel entirely. Older frontends without the option ignore it safely.

Refs #979
2026-09-30 10:52:15 +08:00
Will Miao faeb66a23d feat(recipes): opt-in workflow embedding for widget recipe saves
Add a "Save Recipe with Workflow" action next to "Save Recipe" in the LoRA
widget context menu. It posts the current UI-format graph alongside the save
request so the stored preview embeds it and the recipe can send the graph back
to ComfyUI. Embedding stays opt-in rather than folded into "Save Recipe": the
workflow is by far the largest metadata field and its widget values may carry
sensitive data.

- web/comfyui: new menu entry; saveRecipeDirectly({ embedWorkflow }) posts the
  UI graph and reports the outcome (embedded / skipped) via toasts.
- save_recipe_from_widget handler: reads an optional JSON workflow field so the
  long-standing body-less POST keeps working, including from cached clients.
- RecipePersistenceService.save_recipe_from_widget: embeds the graph through
  the existing optimize_image workflow path, derives has_workflow by detection,
  and skips graphs above MAX_WORKFLOW_EMBED_BYTES with workflow_skipped.
2026-09-29 09:03:26 +08:00
Will Miao 067e605e75 fix: keep bypass/mute state on frontend 1.53+ node shell state (#1123)
ComfyUI frontend 1.53 turned LGraphNode.mode into a prototype accessor
backed by node._state, and serialize() now reads that state directly.
Redefining mode on the instance shadowed the setter, so bypass/mute
never reached the serialized workflow and silently reverted to Always
on save/reload or workflow tab switch.

Add interceptModeChange() in web/comfyui/utils.js: it delegates to the
prototype accessor when one exists (observing changes only), and falls
back to the legacy closure accessor on older frontends. Use it in
lora_loader.js and in the Vue widgets' setupModeChangeHandler, which
covers the LoRA provider/aggregator nodes with the same latent bug.
2026-09-25 18:12:01 +08:00
Will Miao 2f9bd3ee7d feat: support reverse-proxy URL subpaths (llama-swap, SwarmUI) (#1122) 2026-09-24 08:04:00 +08:00
Will Miao c8c84bfc54 feat(loras): warn when widget strength leaves the usage-tips range
The cycler-list payload now carries usage_tips, and the LORAS widget
parses strength_min/strength_max/strength_range into a cached lookup.
Strength inputs (model and clip) turn amber with an explanatory tooltip
when dragged, typed, or stepped outside the recommended range.

Related: https://github.com/willmiao/ComfyUI-Lora-Manager/issues/1090
2026-09-19 05:49:01 +08:00
Will Miao 53fa22f39c fix(lora-loader): preserve repeated spaces inside lora names
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.
2026-09-07 19:20:15 +08:00
Will Miao 93fc28b499 chore(ui): rebuild vue-widgets bundle
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
2026-09-04 19:02:36 +08:00
Will Miao 7afed1a14b fix(ui): cross-link right-click menu in active-filters settings tooltip
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.
2026-09-04 19:02:31 +08:00
Will Miao cf64e5baa8 fix(ui): remove per-node active-filters chip from loras widgets
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.
2026-09-04 19:02:16 +08:00
Will Miao 634ea7f299 fix(ui): keep autocomplete corner buttons clear of the textarea scrollbar
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.
2026-09-04 12:41:31 +08:00
Will Miao 6ba64ebb3c feat(ui): improve /activefilters discoverability on loras nodes
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
2026-09-03 22:42:45 +08:00
Will Miao da71985c3e fix(autocomplete): strip lastAccepted boundary from exported workflows (#1093)
The hidden __lm_autocomplete_meta_* widget persisted lastAccepted
(insertedText/textSnapshot) into exported workflow JSON, leaking old
prompt text even after the user deleted it.

Patch app.graphToPrompt (shared by workflow export, Export API and
queueing) to strip lastAccepted from the serialized result's
widgets_values / widgets_values_named / output inputs. Only the
exported artifact is touched; live node state, undo snapshots,
copy/paste and local saves keep the boundary intact.
2026-09-03 08:29:55 +08:00
Will Miao 77109b3cf8 feat(autocomplete): group relative-path results by folder (#1091)
Autocomplete suggestions were ranked purely by relevance across the whole
library, so same-named loras from different subfolders interleaved and were
hard to tell apart. Results are now bucketed by folder (root first, then
alphabetically, with nested paths sorting naturally) while keeping the
existing relevance ordering within each folder group.
2026-09-02 22:01:44 +08:00
Will Miao 00095a5398 fix(autocomplete): sync active filters via server-side store (#1091)
The LoRA Manager page kept its active filters in localStorage, which the
ComfyUI-side autocomplete read directly. When the two run in different
browsers, origins, or the ComfyUI Desktop Electron shell, localStorage is
not shared and the active-filters search silently did nothing.

The manager page now mirrors its filter state to a server-side in-memory
store (PUT /api/lm/{prefix}/active-filters), pushed on every change via a
storage-listener hook and once on page load. The autocomplete widget sends
only use_active_filters=true, and the relative-paths endpoint injects the
stored filters into the search, with explicit query params taking
precedence.
2026-09-02 14:33:44 +08:00
Will Miao 49704d801c fix: show lora info regardless of toggle state 2026-09-02 11:38:30 +08:00
Will Miao e05046af10 fix(loaders): sanitize invalid control_after_generate values when loading old workflows
Old workflows (saved before the control_after_generate feature) carry a
shorter widgets_values array. The frontend's index-based widget restore
then shifts the old weight_dtype value into the hidden control widget
(leaving an invalid value like 'default') and silently resets
weight_dtype to its default. On graph load, hand the shifted value back
to weight_dtype when it still sits at its default, then reset the
control mode to 'fixed' so old workflows keep loading deterministically.
2026-08-25 08:30:17 +08:00
Will Miao 074d1f2e51 feat(ui): improve tag autocomplete toggle discoverability in prompt nodes
- Add Tag Autocomplete ON/OFF entry to the Prompt (LoraManager) node
  right-click menu, cross-referencing the slash commands
- Show the current autocomplete state (/autocomplete or /noautocomplete
  hint) below the slash command list
- Show a one-time dismissible tip in the suggestion dropdown on first use
- Clarify toggle command labels (Turn autocomplete ON/OFF) and cross-link
  all three entry points in the settings tooltip
- Share the setting write path via setLoraManagerSettingValue()
2026-08-24 12:21:31 +08:00
Will Miao 90be5799e4 fix(ui): preserve group editor scroll position when toggling tags 2026-08-24 08:17:47 +08:00
Will Miao ae461ebc81 fix(registry): replace one %s placeholder per log argument 2026-08-21 21:09:58 +08:00
Will Miao 3ebf256c5d feat(recipe): send embedded recipe workflow to ComfyUI canvas 2026-08-21 21:09:58 +08:00
Will Miao bd380bc1a1 fix(ui): replace stale command abbreviations in autocomplete messages 2026-08-21 09:20:11 +08:00