Files
olivv-cs-ComfyUI-FunPack/app/shell/transport.js
T
digital-garbageandClaude Opus 5 66e4bce345 core, app: a value the queue would refuse, caught before it is queued
ComfyUI refuses the WHOLE graph over one wrong value. v4 hit that with a LoRA
file that had been deleted: a feature that was switched off still stopped every
generation, and the message was "Prompt outputs failed validation" over a dozen
loaders. build() checked wiring and required-ness and never looked at a literal,
so check() called such a pipeline queueable.

A combo value that is not among the node choices, and a number outside the
bounds it declares, are now refused here -- naming the slot and the input.
No bounds means unknown, and unknown is not a refusal.

And when the queue does refuse one, its per-node reasons were captured by
run.js and never read: the status bar showed only the top-level string, because
describe() read `node` and the refusal path sets `nodes`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 17:11:51 +03:00

162 lines
6.1 KiB
JavaScript

// Generate, and everything that happens after it.
//
// One row that owns the whole lifecycle, because the alternative is a button
// here and a progress bar there and a status somewhere else, each with its own
// idea of what is happening. This reads ONE state object and draws it -- so
// "queued" cannot appear next to a finished result, whatever order the messages
// arrive in.
//
// Warnings live here too, beside Generate, rather than in the panel that would
// have caused them: a setting that will do nothing has to be said where the run
// is started, not where it was switched on.
import { composer } from "../composer/composer.js";
import { IDLE, QUEUED, RUNNING, DONE, FAILED, CANCELLED } from "./run.js";
const WORKING = new Set([QUEUED, RUNNING]);
export function createTransport({ onGenerate, onCancel } = {}) {
const generate = composer.button.lg({
label: "Generate", tone: "primary",
onClick: () => { if (onGenerate) onGenerate(); },
});
const cancel = composer.button.md({
label: "Cancel", tone: "ghost",
onClick: () => { if (onCancel) onCancel(); },
});
const progress = composer.progress.bar({ value: 0, max: 100, label: "Generating" });
const status = composer.text.sm({ text: "Ready" });
const bar = composer.actionBar.sticky({
actions: [status, progress, cancel, generate],
});
// Something to say that is not about a run in progress: why the last attempt
// did not become one. It has to survive the next draw(), because a refused
// attempt leaves the run exactly where it was -- idle -- and drawing an idle
// run says "Ready", which wiped the reason a moment after it appeared.
let note = null;
// Held for a reason that is not a run: the page is still working out whether
// it already has one. Kept as a flag rather than left to whoever disabled the
// button, because draw() runs on every state change and each of those would
// otherwise hand the button back -- which is exactly what a subscription
// delivering the current state did, one line after the button was disabled.
let held = false;
// Hidden by attribute rather than removed: the row's shape then does not jump
// as a run starts and finishes, and there is one node to find in a test.
const hide = (handle, yes) => {
if (yes) handle.node.setAttribute("hidden", "");
else handle.node.removeAttribute("hidden");
};
hide(progress, true);
hide(cancel, true);
function draw(state) {
// Anything other than idle means a run answered for itself, and what it
// says outranks a note about an attempt that never started.
if (state.phase !== IDLE) note = null;
const working = WORKING.has(state.phase);
generate.setDisabled(working || held);
generate.setBusy(working);
hide(cancel, !working);
const measured = state.progress && state.progress.max;
hide(progress, !(working && measured));
if (measured) {
progress.node.setAttribute("aria-valuemax", String(state.progress.max));
progress.setValue(state.progress.value);
}
status.setText(note || describe(state));
}
/** Say why nothing was started, or -- with null -- stop saying it. Cleared by
* the next run that says otherwise. */
function say(text, state) {
note = text || null;
status.setText(note || describe(state || { phase: IDLE, images: [] }));
}
/** Stop offering Generate for a reason of the page's own, and say why. */
function hold(reason) {
held = Boolean(reason);
if (reason) say(reason);
generate.setDisabled(true);
}
/** Offer it again. The run's own state decides from here. */
function release(state) {
held = false;
say(null, state);
draw(state);
}
return { node: bar.node, draw, say, hold, release, generate, cancel, progress, status };
}
/**
* Why a run failed, in one line, from whichever of the two shapes it arrived in.
*
* A crash during execution names one node -- `node`. A graph the queue refused
* names as many as were wrong -- `nodes`, an object keyed by node id, each with
* its own reasons. Only the first was ever read, so the whole of the second was
* captured and thrown away: the bar showed ComfyUI's top-level string, which is
* "Prompt outputs failed validation" and says nothing about which of a dozen
* loaders to go and fix.
*/
export function failure(error) {
if (!error) return "the run failed";
const first = firstNodeError(error.nodes);
if (first) return first;
const where = error.node ? `${error.node}: ` : "";
return `${where}${error.message || "the run failed"}`;
}
function firstNodeError(nodes) {
if (!nodes || typeof nodes !== "object") return null;
for (const [id, entry] of Object.entries(nodes)) {
const reasons = entry && Array.isArray(entry.errors) ? entry.errors : [];
for (const reason of reasons) {
const what = reason && (reason.message || reason.type);
if (!what) continue;
// The class type as well as the id: a node id is a number in ComfyUI's
// own graphs and a slot name in ours, and neither on its own is much to
// go on when the message is "Value not in list".
const which = entry.class_type ? `${entry.class_type} (${id})` : id;
const detail = reason.details ? ` — ${reason.details}` : "";
return `${which}: ${what}${detail}`;
}
}
return null;
}
/** One line saying where the run is. The only place these words are decided. */
export function describe(state) {
switch (state.phase) {
case QUEUED:
return "Queued";
case RUNNING: {
const where = state.node ? `node ${state.node}` : "working";
if (state.progress && state.progress.max) {
return `${where} — ${state.progress.value} of ${state.progress.max}`;
}
return where.charAt(0).toUpperCase() + where.slice(1);
}
case DONE:
// The count, because a graph that saved nothing finished successfully and
// produced no picture, and those must not read the same.
return state.images.length
? `Done — ${state.images.length} result${state.images.length === 1 ? "" : "s"}`
: "Done, but nothing was saved";
case FAILED:
return failure(state.error);
case CANCELLED:
return "Cancelled";
default:
return "Ready";
}
}