feat: Builder UI Run Settings — top-of-UI placement + 6 generator-node overrides

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>
This commit is contained in:
Jason Hoku
2026-05-11 09:21:14 -07:00
co-authored by Claude Opus 4.7
parent 4821433b24
commit 7a2c3c905e
4 changed files with 159 additions and 2 deletions
+36
View File
@@ -832,6 +832,42 @@ class UltimateConfigBuilder:
image_format = state.get("image_format", "webp")
if image_format and image_format != "webp":
session_settings["image_format"] = image_format
# Generator-node override settings — emit only when non-default so the
# widget value on the SamplerGridTester node wins for users who haven't
# explicitly set them in the Builder UI. When the Generator widgets are
# eventually removed, these can be made unconditional.
ov_existing = state.get("overwrite_existing", False)
if ov_existing:
session_settings["overwrite_existing"] = True
flush_be = state.get("flush_batch_every", 4)
try:
flush_be = int(flush_be)
except (TypeError, ValueError):
flush_be = 4
if flush_be != 4:
session_settings["flush_batch_every"] = flush_be
lora_tw = state.get("lora_triggerwords_mode", "None")
if lora_tw and lora_tw != "None":
session_settings["lora_triggerwords_mode"] = lora_tw
save_cc = state.get("save_conditioning_cache_to_file", False)
if save_cc:
session_settings["save_conditioning_cache_to_file"] = True
en_mc = state.get("enable_model_cache", False)
if en_mc:
session_settings["enable_model_cache"] = True
vae_bs = state.get("vae_batch_size", 4)
try:
vae_bs = int(vae_bs)
except (TypeError, ValueError):
vae_bs = 4
if vae_bs != 4:
session_settings["vae_batch_size"] = vae_bs
if session_settings:
output_obj["_session_settings"] = session_settings
+31
View File
@@ -292,6 +292,37 @@ class SamplerGridTester:
except Exception as e:
print(f"[GridTester] ⚠️ Error parsing configs_json for distribution: {e}")
# Builder UI Run Settings override widget values when present.
# When a user sets these in the Builder UI, _session_settings carries
# them through configs_json and they take precedence over what's
# wired to the SamplerGridTester widgets. Generator widgets are still
# the fallback for backward compat.
if session_settings:
if "overwrite_existing" in session_settings:
overwrite_existing = bool(session_settings["overwrite_existing"])
print(f"[GridTester] ⚙️ overwrite_existing overridden by Builder UI: {overwrite_existing}")
if "flush_batch_every" in session_settings:
try:
flush_batch_every = int(session_settings["flush_batch_every"])
print(f"[GridTester] ⚙️ flush_batch_every overridden by Builder UI: {flush_batch_every}")
except (TypeError, ValueError):
pass
if "lora_triggerwords_mode" in session_settings:
lora_triggerwords_mode = str(session_settings["lora_triggerwords_mode"])
print(f"[GridTester] ⚙️ lora_triggerwords_mode overridden by Builder UI: {lora_triggerwords_mode}")
if "save_conditioning_cache_to_file" in session_settings:
save_conditioning_cache_to_file = bool(session_settings["save_conditioning_cache_to_file"])
print(f"[GridTester] ⚙️ save_conditioning_cache_to_file overridden by Builder UI: {save_conditioning_cache_to_file}")
if "enable_model_cache" in session_settings:
enable_model_cache = bool(session_settings["enable_model_cache"])
print(f"[GridTester] ⚙️ enable_model_cache overridden by Builder UI: {enable_model_cache}")
if "vae_batch_size" in session_settings:
try:
vae_batch_size = int(session_settings["vae_batch_size"])
print(f"[GridTester] ⚙️ vae_batch_size overridden by Builder UI: {vae_batch_size}")
except (TypeError, ValueError):
pass
return run_generation_loop(
self,
ckpt_name, positive_text, negative_text, seed, denoise, vae_batch_size,
@@ -6344,6 +6344,12 @@ export function renderCooldownSection(node, container) {
export function renderRunSettingsSection(node, container) {
if (node.state.start_at_job === undefined) node.state.start_at_job = 0;
if (node.state.image_format === undefined) node.state.image_format = "webp";
if (node.state.overwrite_existing === undefined) node.state.overwrite_existing = false;
if (node.state.flush_batch_every === undefined) node.state.flush_batch_every = 4;
if (node.state.lora_triggerwords_mode === undefined) node.state.lora_triggerwords_mode = "None";
if (node.state.save_conditioning_cache_to_file === undefined) node.state.save_conditioning_cache_to_file = false;
if (node.state.enable_model_cache === undefined) node.state.enable_model_cache = false;
if (node.state.vae_batch_size === undefined) node.state.vae_batch_size = 4;
const section = document.createElement("div");
section.className = "cb-section full-width";
@@ -6414,6 +6420,73 @@ export function renderRunSettingsSection(node, container) {
content.appendChild(fmtDiv);
content.appendChild(fmtDesc);
// Helper: renders a labeled row with a "?" tooltip icon, binding to node.state[stateKey].
function _addRunSetting(content, opts) {
// opts: { stateKey, label, tooltip, kind: "bool"|"int"|"select", options?, min?, max?, defaultValue }
const row = document.createElement("div");
row.style.cssText = "margin-top: 10px; display: flex; align-items: center; gap: 8px;";
const label = document.createElement("label");
label.textContent = opts.label + ":";
label.style.cssText = "font-size: 12px; color: #ccc; white-space: nowrap;";
const help = document.createElement("span");
help.textContent = "?";
help.title = opts.tooltip;
help.style.cssText = "display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: #2a2a2a; color: #8af; font-size: 10px; font-weight: bold; cursor: help; user-select: none;";
let input;
if (opts.kind === "bool") {
input = document.createElement("input");
input.type = "checkbox";
input.checked = !!node.state[opts.stateKey];
input.style.cssText = "transform: scale(1.2); cursor: pointer;";
input.onchange = () => { node.state[opts.stateKey] = input.checked; node.saveState(); };
} else if (opts.kind === "int") {
input = document.createElement("input");
input.type = "number";
if (opts.min !== undefined) input.min = opts.min;
if (opts.max !== undefined) input.max = opts.max;
input.value = node.state[opts.stateKey] !== undefined ? node.state[opts.stateKey] : opts.defaultValue;
input.style.cssText = "width: 70px; background: #1a1a1a; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 4px 6px; font-size: 12px;";
input.onchange = () => {
const v = parseInt(input.value);
node.state[opts.stateKey] = isNaN(v) ? opts.defaultValue : v;
node.saveState();
};
} else if (opts.kind === "select") {
input = document.createElement("select");
input.style.cssText = "background: #1a1a1a; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 4px 6px; font-size: 12px;";
(opts.options || []).forEach(o => {
const opt = document.createElement("option");
opt.value = o;
opt.textContent = o;
if (o === (node.state[opts.stateKey] || opts.defaultValue)) opt.selected = true;
input.appendChild(opt);
});
input.onchange = () => { node.state[opts.stateKey] = input.value; node.saveState(); };
}
row.appendChild(label);
row.appendChild(help);
row.appendChild(input);
content.appendChild(row);
}
_addRunSetting(content, { stateKey: "overwrite_existing", label: "Overwrite Existing", kind: "bool",
tooltip: "True = Re-run everything. False = Skip already generated images (Resume)." });
_addRunSetting(content, { stateKey: "flush_batch_every", label: "Flush Batch Every", kind: "int", min: 0, max: 64, defaultValue: 4,
tooltip: "Update dashboard every X images. 0 = Use VAE Batch Size." });
_addRunSetting(content, { stateKey: "lora_triggerwords_mode", label: "LoRA Triggerwords Mode", kind: "select",
options: ["None", "Append To End", "Append To Start", "Read From Config"], defaultValue: "None",
tooltip: "None = Don't fetch/append trigger words. Append To End = Add triggers at end of prompt (default behavior). Append To Start = Add triggers at start of prompt. Read From Config = Use lora_triggerwords_append_settings in config JSON to specify per-lora placement." });
_addRunSetting(content, { stateKey: "save_conditioning_cache_to_file", label: "Save Conditioning Cache To File", kind: "bool",
tooltip: "Save CLIP conditioning cache to disk. Useful when experimenting with the same prompts/models — skips text encoding on resume. WARNING: Can create very large files in output/benchmarks. Automatically disabled when optional inputs (model/clip/conditioning) are connected." });
_addRunSetting(content, { stateKey: "enable_model_cache", label: "Enable Model Cache", kind: "bool",
tooltip: "Experimental: intelligent model/LoRA caching with async background preloading. Speeds up generation when switching LoRAs frequently. Loads cached LoRAs from RAM instead of disk. Disable to reduce RAM/VRAM usage." });
_addRunSetting(content, { stateKey: "vae_batch_size", label: "VAE Batch Size", kind: "int", min: -1, max: 64, defaultValue: 4,
tooltip: "How many images to encode/decode per VAE pass. Lower = less VRAM. -1 = process all at once. Default: 4." });
section.appendChild(header);
section.appendChild(content);
container.appendChild(section);
@@ -6583,6 +6656,10 @@ export async function renderUI(node, availableLoras, modelLists, loraFolders, av
// Global Prompts Section
renderGlobalPromptsSection(node, mainContent);
// Run Settings Section — session-level generator overrides, shown at top
// so users see them before diving into config arrays.
renderRunSettingsSection(node, mainContent);
// Config Arrays Section
const configSection = document.createElement("div");
configSection.className = "cb-section full-width";
@@ -6774,7 +6851,6 @@ export async function renderUI(node, availableLoras, modelLists, loraFolders, av
// Session-level settings (applies to all configs)
renderUpscalingSection(node, mainContent, modelLists);
renderCooldownSection(node, mainContent);
renderRunSettingsSection(node, mainContent);
// Output Preview — rendered as a SIBLING of layoutWrapper (not inside
// mainContent), so it sits as a fixed/resizable panel at the bottom of
+15 -1
View File
@@ -189,7 +189,13 @@ app.registerExtension({
every_n: 1,
clear_vram: false
},
image_format: "webp"
image_format: "webp",
overwrite_existing: false,
flush_batch_every: 4,
lora_triggerwords_mode: "None",
save_conditioning_cache_to_file: false,
enable_model_cache: false,
vae_batch_size: 4
};
// Create HTML container immediately
@@ -267,6 +273,14 @@ app.registerExtension({
});
}
// Migration: ensure top-level Run Settings fields exist
if (this.state.overwrite_existing === undefined) this.state.overwrite_existing = false;
if (this.state.flush_batch_every === undefined) this.state.flush_batch_every = 4;
if (this.state.lora_triggerwords_mode === undefined) this.state.lora_triggerwords_mode = "None";
if (this.state.save_conditioning_cache_to_file === undefined) this.state.save_conditioning_cache_to_file = false;
if (this.state.enable_model_cache === undefined) this.state.enable_model_cache = false;
if (this.state.vae_batch_size === undefined) this.state.vae_batch_size = 4;
this.saveState();
this.renderUI();
}