The state_to_configs_json fix (a3ad111) correctly omits the "vae" key
when nothing is selected, but the orchestrator still crashed when a
config carried "vae": "None" from any other path — legacy saved state,
Revise modal edits, distributed worker job specs, etc. Even a fresh
Builder UI node could trigger this if any consumer pushed "None" back
into the entry.
Defense in depth: at all 4 read sites in generation_orchestrator.py,
normalize "None"/"none"/empty/falsy values to "Default" before the
"!= Default" gate that decides whether to load a per-config VAE. With
"Default" the orchestrator falls back to the checkpoint's bundled VAE
instead of calling load_vae_by_name("None") which raises
FileNotFoundError.
Sites normalized: check_if_job_completed VAE comparison (~line 227),
first-conf VAE load (~line 1029, the user's crash trace), per-config
VAE switching in the main loop (~line 1454), distributed-mode VAE
switching (~line 2885).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime error: "'PatchModelAddDownscale' object has no attribute 'patch'".
Current ComfyUI (verified in this install's comfy_extras/nodes_model_
downscale.py) ships PatchModelAddDownscale as a V3 io.ComfyNode subclass
with a classmethod execute() returning io.NodeOutput. The agent's
original .patch() instance call only works on the legacy V1 class.
Now dispatches V3-first (subclass check + cls.execute()) with a V1
fallback (FUNCTION attribute, default "patch") for older builds. Unwraps
NodeOutput.args[0] for V3, tuple[0] for V1, bare value otherwise. Same
pattern as ltx_video_generation._call_node / _unwrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new SUB-GROUP 4 in the Builder UI's "Extra Model & Sampling
Options" section with all 7 params from ComfyUI's built-in
PatchModelAddDownscale node:
- block_number (int, default 3)
- downscale_factor (float, default 2.0)
- start_percent / end_percent (float 0-1, default 0.0 / 0.35)
- downscale_after_skip (bool, default true)
- downscale_method / upscale_method (combo: bicubic/nearest-exact/
bilinear/area/bislerp, default bicubic)
Toggle checkbox controls whether the patch is applied. State plumbs:
config_array.use_deep_shrink/* (JS) -> state_to_configs_json -> per-
config entries -> expand_configs base_combos -> generate_image -> looks
up PatchModelAddDownscale via NODE_CLASS_MAPPINGS and calls .patch() to
patch the model before sampling. Logs an "Applied
PatchModelAddDownscale" line on each gen for visibility.
Mirrors the existing model_sampling_override / use_advanced_sampling
patterns for consistency. Hidden on LTX video config arrays since the
patch targets the SDXL/SD UNet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User feedback: 1 is a more sensible default for typical generation —
smaller VAE batches keep VRAM lower, and flushing every gen makes the
dashboard show progress immediately.
Updated everywhere:
- sampler_node run_tests fallback defaults (post-widget-removal)
- conf-builder-main.js initial state + migration backfill
- conf-builder-config-management.js renderRunSettingsSection
state-init defaults + _addRunSetting defaultValue
- config_builder_node.py state_to_configs_json emit fallback +
except-clause fallback for both fields
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported that cards with many stacked LoRAs grew very tall and
broke the 1-9 quick-favorite hotkeys.
Root cause: createCard rendered all stacked LoRAs as a multi-line block
("name1\nname2\nname3\n..."). Many LoRAs = tall card. The virtual
scroller positions rows on a fixed stride, so tall cards overflowed
into adjacent rows. _quickFavoriteByPosition clusters visible cards
into rows by y-center with rowTolerance=20 — overflowing cards land
in the wrong row group and the Nth-card mapping breaks.
Fix: stacked LoRAs now render as "<first_name> +N more" on a single
line (e.g. "DMD2 +4 more"). Full list still available via tooltip on
hover (title attribute). Card heights stay uniform and the row
clustering works correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the widget cleanup (30e3aa9), the SamplerGridTester is nearly
empty — just one required input (configs_json), six optional sockets,
and a single output. New users won't know how to wire it.
Adds a small footnote (~44px tall) at the bottom of the node via a
non-serialized DOM widget:
"Connect the configs_json from the Builder node to this node, and
connect this node to the Dashboard node"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
session_name is now carried inside configs_json via _session_settings
(emitted unconditionally as of e570f6b, honored by sampler_node.run_tests).
The dedicated output socket was redundant — and now that the
SamplerGridTester's session_name widget is gone (commit 30e3aa9),
nothing on the downstream side reads it anyway.
Workflows that wired BuilderUI.session_name -> somewhere will have that
wire silently dropped by ComfyUI on load. No action needed for users
who only use the configs_json wire (the typical setup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two UX defaults users hit on first drop:
1. Default node size was ~200x100 (Comfy's default), making the HTML
overlay invisible. Bumped to 720x900 on fresh create. Existing
workflows keep their saved size (only the max-with-current is
applied, gated by an _uscgSizeInitialized flag so the size override
doesn't fight user resizes).
2. Output Preview panel defaulted to 450px which dominated the UI on
smaller nodes. Reduced to 175px (~8 lines of JSON visible at the
11px/1.4-leading default font). The drag handle still lets users
grow it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes 15 widgets from the SamplerGridTester INPUT_TYPES. The Builder
UI is now the single source for run-level settings via _session_settings
in configs_json. Optional inputs (6 inputs) and the hidden unique_id are
kept; everything else is replaced by hardcoded defaults at the top of
run_tests.
Removed widgets:
- ckpt_name, positive_text, negative_text, seed, denoise
- vae_batch_size, resolutions_json, session_name
- overwrite_existing, flush_batch_every, add_random_seeds_to_gens
- lora_triggerwords_mode, remote_vae_endpoint
- save_conditioning_cache_to_file, enable_model_cache
Defaults set internally (overridden by _session_settings when present):
- ckpt_name: first available checkpoint (fallback so standalone use with
"model": "Default" does not crash the orchestrator with None)
- positive_text="", negative_text="", seed=0, denoise="1.0"
- vae_batch_size=4, resolutions_json='[[1024, 1024]]', session_name="my_session"
- overwrite_existing=False, flush_batch_every=4, add_random_seeds_to_gens=0
- lora_triggerwords_mode="None", remote_vae_endpoint="None"
- save_conditioning_cache_to_file=False, enable_model_cache=False
Existing workflows: ComfyUI silently drops widget values for removed
INPUT_TYPES keys. Workflows that ran on prior versions will use the new
internal defaults; users who customized those widgets need to set the
same values in the Builder UI Run Settings instead (the previous commit
made all Builder UI run-settings authoritative so they always override).
UX: the Generator node is now a near-empty box with one required input
(configs_json) and 6 optional sockets. The user-facing controls all
live in the Builder UI. This simplifies the graph and removes a
recurring source of confusion (widgets that were silently overridden).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues in 7a2c3c9:
1. start_at_job's onchange handler used setDirtyCanvas() instead of
saveState() — changes never reached the lora_config widget value.
Fixed to call saveState().
2. The 6 run-settings emissions were gated on "non-default" values
(e.g. lora_triggerwords_mode only emitted when != "None"). Result:
a user setting Builder UI to its default could not override the
Generator widget if the user had previously set that widget to
something non-default. Builder UI is now authoritative — emit all
values unconditionally. start_at_job and image_format also
switched to unconditional emission for consistency.
3. session_name had no plumbing path. Added emission in
state_to_configs_json and an override branch in sampler_node so
the Builder UI's session_name field overrides the generator widget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in state_to_configs_json's VAE emission:
1. Bypass (unchecked) VAEs were still sent through — vae_bypass_states
was never consulted in the filter. Now filtered alongside the
None/empty check.
2. When no VAE remained (empty list, all bypassed, or only "None"),
the config emitted "vae": "None" — the orchestrator interpreted
that as a model filename and threw "vae model with filename 'None'
not found". Now the "vae" key is OMITTED entirely so the generator
falls back to its default behavior (no per-config VAE override),
matching the pattern already used for text_encoders/clip_type/etc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User wanted Run Settings to be the FIRST section, above everything
including Distribution Settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of consolidating run-level config into the Builder UI. The
Builder UI's "Run Settings" section is now rendered at the TOP of the UI
(was previously below config arrays) and gains six new fields with help
tooltips:
- overwrite_existing (bool, default false)
- flush_batch_every (int, default 4)
- lora_triggerwords_mode (enum: None / Append To End / Append To Start /
Read From Config, default None)
- save_conditioning_cache_to_file (bool, default false)
- enable_model_cache (bool, default false)
- vae_batch_size (int, default 4, range -1..64)
State plumbs through state_to_configs_json -> _session_settings ->
sampler_node.run_tests -> run_generation_loop. Builder UI values
override the corresponding widgets on UltimateSamplerGrid when set
(non-default). Widget values remain the fallback so existing workflows
keep working without changes.
Logs an "⚙️ <name> overridden by Builder UI: <value>" line whenever an
override fires, so it's obvious in the console.
Next: once the user has tested this thoroughly, the corresponding
widgets on the SamplerGridTester node can be removed/hidden to
simplify the generator UX (deferred — coordinate with user).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lightbox-click revert (4a318e6) stripped both onclick AND ondblclick
from the <img> tag. The intent was to remove the accidental-tap-opens-
lightbox behavior, but the double-click-to-favorite was collateral damage.
Videos kept their ondblclick handler; images lost it.
Restoring ondblclick="toggleFavorite(this)" on <img>. No onclick — single
click still does nothing (panning safe).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These changes belong with the earlier "preview panel uses /configbuilder/preview"
work (b6e2344) but were dropped from that commit. Folding in now:
- Adds .cb-preview-panel + resize-handle + header/content CSS used by the
renderPreviewSection function shipped in 991631a.
- Removes the obsolete addNavIcon("📄", "JSON Preview", ...) sidebar entry.
- Cleans the cb-sec-preview ID out of the scroll-spy section list.
No behavior change beyond what was already committed — fills in the
visual/CSS gap the committed JS depends on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These keys legitimately aren't set in pre-orchestrator configs:
- seed: filled by expand_configs (node-level seed widget or per-entry override)
- denoise: filled by run_generation_loop from the denoise widget
- clip_type: only set when model_type != "checkpoint"
The validator was a stale leftover from the pre-single-source-of-truth era.
Trimming false positives. Still warns on actual typos / dropped fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported that changing seed in the Revise modal had no effect —
the new seed was overwritten and the orchestrator's check_if_job_completed
matched the original item's seed, skipping the gen.
Root cause: config_utils.expand_configs unconditionally wrote
"seed": <global node seed> for every config, ignoring entry["seed"]
from the configs_json. Builder UI doesn't emit entry["seed"] (it relies
on the node-level seed widget), so this regression only surfaced when
something else explicitly set per-entry seed — exactly what triggerGen
does.
Fix: use entry["seed"] when present and non-empty, otherwise fall back
to the global seed. Builder UI flow unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two QoL tweaks:
- CANCEL button text → EXIT (clearer when paired with the new "stays open"
behavior — there's nothing to "cancel" after queuing).
- Generate New no longer closes the modal. The QUEUED! flash on the button
is enough confirmation. Users can keep tweaking sampler/cfg/prompt and
queueing additional revisions of the same image without re-opening.
Close manually via EXIT or Esc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported edited sampler/cfg/steps/prompt were ignored — the original
config ran and got skipped as already-existing.
Root cause: configs_json on UltimateSamplerGrid is declared as both a
widget AND an input socket. In the typical setup (UltimateConfigBuilder
wired into UltimateSamplerGrid), the input link OVERRIDES the widget at
queue time. The previous triggerGen set widget.value but Comfy used the
upstream node's output instead.
Fix: detect the connected link, disconnect before setting the widget
value, queue the prompt, then restore the link after queue submission
completes. Defensively also update widget.inputEl, widget.element,
fire widget.callback, and setDirtyCanvas so all known widget-update
channels are hit.
Adds clear error messaging when no UltimateSamplerGrid node exists,
multi-node warning when there's more than one, and console logs at each
critical step for future debugging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four bugs in 111e24d:
1. Dropdown positions were zoom-dependent / offset because JS overrode
the HTML's "position: absolute; top: 100%" with "position: fixed" +
getBoundingClientRect() coords. Inside the dashboard iframe (with
Comfy's transformed canvas as ancestor), position:fixed becomes
relative to that ancestor instead of viewport. Fix: wrap button +
menus in a position:relative anchor div (#quick-filter-anchor), drop
all JS coord setting, let CSS handle positioning.
2. Click-outside handler stayed registered after items used
e.stopPropagation, so reopening required toggling another UI element
first. Fix: named _qfMenusOutsideHandler with explicit
_qfAttachOutsideHandler/_qfDetachOutsideHandler pair; capture phase
(third arg true) bypasses stopPropagation; _closeQuickFilterMenus
now calls _qfDetachOutsideHandler; addQuickFilter calls
_closeQuickFilterMenus upfront.
3. Value menu was anchored to a hidden element (type menu hidden BEFORE
value menu opened), so getBoundingClientRect returned zeros and value
menu rendered at viewport (0, 6). Fixed by Bug 1 (CSS positioning);
anchorEl param dropped from _openQuickFilterValueMenu.
4. lora_strength wasn't in _qfAllTypes / _qfFacetableTypes /
_qfTypeLabels / _qfExtractItemValue / _quickFilterMatches. Now
selectable: extracts first lora's first strength from item.lora,
compares with 1e-6 float tolerance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third filter mode alongside main buttons / search / logic.
User clicks "+ Add Quick Filter" -> picks a type (LoRA/CFG/etc.) ->
picks a value -> adds a chip. Each chip can be expanded to show
"sub-filters": values of OTHER filter types that exist within the
subset matching this chip's primary. Click a sub-filter button to
add it as another chip (ANDed with existing).
Designed as faceted search: drill-down by selecting facets one at
a time. Each chip is independent and ANDed with the others (matching
the GitHub discussion's "logical && between boxes" semantic).
LoRA matching handles stacks ("name1 + name2"), so a "DMD2"
quickFilter matches items where DMD2 is one of multiple stacked
loras. Numeric types (cfg/steps/denoise/seed) use 1e-6 float
tolerance matching the Logic Filter convention.
Reset All Filters and the R key both clear quickFilters along with
the other filter mechanisms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: when 25/26 items already exist and only 1 new is being
generated, dashboard showed "job 14/26" or similar (skip path doesn't
update progress). Now displays effective progress: existing_count +
new_completed / total. So a session with 25 pre-existing shows 25/26
immediately, then 26/26 after the 1 new gen.
- Adds new_completed counter (separate from loop-index current_job)
- Replaces current_job with existing_count+new_completed in dashboard-
facing log/send_sync (lines 2107, 2114, 2123, 2133)
- Sends an initial progress update right after pre-flight so the user
sees the correct "already done" count before the first gen, clearing
any stale values from a prior run
- Other current_job references (LTX dispatch, image-gen log, start_at_job
gate, periodic save, final send, distributed mode) keep iteration-
index semantic — different contexts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: still triggers accidentally during panning. Reverts the
click-to-open feature added in 8790f97 (with timer-based fix in
4672116). REVISE button remains the only way to open the modal.
In-modal keyboard navigation (arrow keys, spacebar, 1-9 favorite, ESC)
added in 8790f97 stays intact — that part of the lightbox feature is
useful and uncontroversial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 490c519 added a webp/png/jpg dropdown but only flush_batch_with_vae
honored it. Three other save sites still hardcoded WEBP:
- flush_batch_with_remote_vae (image_generation.py)
- upscale_runner.py (3 sites in inline + deferred upscale paths)
- distribution_worker.py (PIL bytes shipped back to master)
Now all save sites read image_format from session_settings (orchestrator)
or the job spec (distributed) and match the format/extension.
Changes:
- remote_vae.py: RemoteVAEDecodeWorker accepts image_format kwarg; _worker
uses it to pick format/extension at save time (the real save site for the
remote VAE path — flush_batch_with_remote_vae only queues to the worker)
- generation_orchestrator.py: passes image_format to all RemoteVAEDecodeWorker
constructors (initialize_remote_vae + 3 per-config sites); adds image_format
to all 8 existing_data["meta"] writes so upscale_runner can read it from disk
- upscale_runner.py: reads image_format from manifest meta at thread start;
uses _fmt/_ext locals at all 3 save sites (seedvr2, latent-decode, PIL)
- distribution_manager.py: injects _session_image_format into worker job
config alongside existing _session_cooldown passthrough
- distribution_worker.py: reads _session_image_format from config; encodes
bytes in the chosen format; stamps _image_ext on meta so master uses it
- distribution_routes.py: pops _image_ext from meta to build filename with
correct extension before writing bytes to disk
Default behavior unchanged: all paths fall back to webp if image_format
is absent or unrecognised.
Code review found this in 490c519.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review found multiple inaccuracies:
- Instructed contributors to update a non-existent convertStateToConfigs()
JS function (was deliberately removed — single source of truth lives in
Python state_to_configs_json).
- Wrong line numbers for generate_config (~465 → ~837) and default state
(~80 → ~103); onConfigure migration reference replaced with the actual
async init block at ~484.
- Overstated safe_print module shadow scope (only 2 files, not all).
- Referenced missing LICENSE file (pyproject.toml declares it but the file
does not exist in the repo root).
Replaced with correct architectural guidance pointing to
state_to_configs_json, accurate line numbers, scoped safe_print claim,
and license info from pyproject.toml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Card <img> previously had both onclick=openM and ondblclick=toggleFavorite.
Browsers fire click twice before dblclick, so double-clicking toggled
favorite AND opened the modal (twice). Mobile tap-to-scroll also
accidentally opened the lightbox.
Replace with a 250ms-delayed click handler (_cardImgClick) that
distinguishes single (open modal) from double (favorite). Remove
unused favIcon variable in the modal 1-9 keypress handler.
Code review found these in commit 8790f97 (lightbox feature).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseFloat-then-=== fails for common stored values (e.g. cfg=7.0
filter against item.cfg=7.0000000001 from float arithmetic). Use
1e-6 epsilon for both = and != operators.
Code review found this in commit bcadb7e (Logic Filter feature).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking a card image now opens the Revise modal as a lightbox. Adds a
navigation bar with prev/next buttons and a position counter. Arrow keys
(← →), Space/Shift+Space navigate between filtered items; 1-9 favorites
the current image. Grid keyboard shortcuts are suppressed while the modal
is open to prevent conflicts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Logic Filter panel in the filter modal with field/operator/value
dropdowns (supports lora_strength, cfg, steps, denoise, width, height,
seed). Multiple filters apply with AND logic. Chips display active filters
with one-click removal. Integrates into data pipeline via matchesLogicFilters()
helper. Reset All Filters also clears logic filters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Reset All' button to the filter modal header that re-selects all
filter buttons and clears search/logic filters. Updates the R key shortcut
to also reset the filter UI before reloading cards so visual state stays
consistent with the data.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Delete All Rejected Items' button in the cog menu next to the
existing 'Delete All Non Favorited Items' button. Adds the matching
/config_tester/delete_rejected backend endpoint that removes rejected
item files and updates the manifest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dropdown to the Builder UI's Run Settings section letting users
pick the saved image file format. Default remains webp (current
behavior). State plumbed through state_to_configs_json ->
_session_settings -> orchestrator -> save_image_to_disk.
PNG users will see lossless saves; JPG users smaller files at quality 95.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Welcomes PRs, documents dev setup, test commands, key architecture
notes (single-source-of-truth transformer, manifest field names,
V1/V3 node dispatch), and a PR checklist.
Closes user request from GitHub feedback thread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scans the planned configs list once before the generation loop and
counts how many already have output files on disk (the same skip path
used by check_if_job_completed). Displays the count in the running
ETA line: "job X/Y (P%) | ETA: ... | N existing".
Helps users gauge how much actual work remains when re-running a
session that's been partially completed in a prior run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CSS rules and variables (--accent-steps/cfg/upscale/media) were already
added in 2c3851e; user reporting the bug was viewing a stale inlined dashboard
HTML generated before that commit. Since html_generator.py inlines report.css
at generation time, regeneration is required for CSS edits to take effect.
Added belt-and-suspenders box-shadow: inset to the four filter types so that
even if a theme override or cached page interferes with background, the inset
ring remains visible. Added a code comment documenting the regeneration
requirement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user types '0.5, 1.0' they want 2 outputs (lora at 0.5, lora
at 1.0 — both model and clip mirroring), not 4 outputs (Cartesian
m×c). The bug was that we wrote both _model AND _clip side-channel
arrays, so Python emitted 'name:[m]:[c]' which the orchestrator
correctly interpreted as Cartesian.
JS: collapse the two-input compare section to a single 'Strengths:'
input. Never write _clip to lora_weight_arrays.
Python: process_lora_array now emits 'name:[m_arr]' (no clip part)
when only _model array is present. The generator's existing
parse_lora_definition defaults clip=model when the third segment
is missing, giving N configs with locked semantics.
Drops the Cartesian path entirely. Backward compat retained: if a
saved workflow has both arrays (legacy), the dual-bracket form is
still emitted so the orchestrator can Cartesian-expand as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the two separate '+ Compare Strengths' / '+ Compare CLIP'
buttons with a single button that toggles a section showing two
labeled inputs ('Model Strengths:' and 'Clip Strengths:'). When
Strength Lock is ON, only the Model input shows (CLIP mirrors
automatically — same as today).
Also fixes button-text-not-updating bug: previously the button
required a full re-render to swap between '+ Compare Strengths' and
'✕ Array' text, so toggling the input visible left the button stuck
at '+ Compare Strengths' until page reload. Activation now persists
arrays as [1] (Python passes through unchanged) and uses array
existence rather than length>1 as the active indicator, so reloads
correctly restore the active state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
convertStateToConfigs() was deleted in the previous commit; the JS
preview now POSTs to the new endpoint and Python is the sole transformer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces synchronous JS convertStateToConfigs (~280 lines) with a
debounced (800ms) fetch to the new endpoint. Preview now renders
literally what generate_config produces — drift impossible by
construction.
Stale-state badge surfaces network failures without blocking editing.
Loading placeholder appears during the first-paint debounce window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both runtime (generate_config) and preview now invoke
state_to_configs_json — single source of truth, drift-proof.
The Builder UI will fetch this endpoint instead of running a
parallel JS transformer (next commit removes that JS code).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers single-lora pass-through, multi-lora combine with ' + ',
folder ref un-expansion, folder+brackets excluded from combine,
bypassed lora exclusion, and ltx_video model_type propagation.
Also tightens the existing bracket-array test from substring match
to exact string equality and removes the unused pytest import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates the JS<->Python drift bug class by making one Python function
the source of truth for state -> configs_json. Both the runtime
(generate_config) and the new preview endpoint (next commit) call it.
Adds golden test for the lora-strength-arrays bracket case that just
broke (regression guard).
Also loads the real config_builder_node module in conftest.py (instead
of a bare stub) so that tests against UltimateConfigBuilder methods work
correctly. Stubs network_utils before loading to satisfy the relative
import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>