fix: a failed run says which node and why; one queued run per project across tabs; Sampler settings say when not saved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-06 02:24:55 +03:00
co-authored by Claude Opus 5.5
parent 9f8d8034e1
commit ce60e4d9ad
5 changed files with 57 additions and 2 deletions
+18
View File
@@ -141,3 +141,21 @@ test("an edit still on its way when Generate is clicked (the Story box splitting
await app.runner.units(["a"]);
assert.deepEqual(seen, ["typed last"]);
});
test("a run that fails while running says which node and ComfyUI's own reason", async () => {
const { app } = rig("idle", { generate: async () => true, waitForTerminal: () => Object.assign(Promise.resolve("failed"), { cancel() {} }) });
app.generate.run.state.error = { node: "SaveVideo", message: "Saving image outside the output folder is not allowed." };
const before = document.body.textContent;
await app.runner.units(["a"]);
assert.match(document.body.textContent.replace(before, ""), /failed in SaveVideo: Saving image outside the output folder/);
});
test("a project that already has a run in ComfyUI's queue (another tab) is not queued again, and the click says why", async () => {
let queued = 0;
const { app } = rig("idle", { generate: async () => { queued += 1; return true; } });
app.api.projectQueued = async (id) => id === "P";
const before = document.body.textContent;
await app.runner.units(["a"]);
assert.equal(queued, 0);
assert.match(document.body.textContent.replace(before, ""), /already has a run in ComfyUI's queue/);
});
+7 -1
View File
@@ -60,6 +60,8 @@ export default {
// A project's pipeline still going in is the one this run uses, not the last one's.
if (app.pipeline.settled && !(await app.pipeline.settled())) { tell("Not started: this project's pipeline is still loading. ComfyUI is slow to answer; try again in a moment."); return; }
if (app.pipelineOwned && !app.pipelineOwned()) { tell(`Not started: ${(app.pipelineWhy && app.pipelineWhy()) || "this project's pipeline is not loaded yet."}`); return; }
// The page's own guard does not see another tab: ComfyUI's queue does. One GPU job per project at a time.
if (app.api.projectQueued && (await app.api.projectQueued(pid))) { tell("This project already has a run in ComfyUI's queue (another tab or window?). Wait for it, or Stop it there."); return; }
// The pipeline as it is NOW, at the click: every shot of this run uses it, whatever is opened or edited meanwhile.
const frozen = structuredClone(app.pipeline.slots() || []);
if (times > 1 && !rolesAt(frozen, "generation.seed").length) { tell("This pipeline has no seed input the app controls, so the takes would all come out the same."); return; }
@@ -96,7 +98,11 @@ export default {
const end = await done;
if (end === g.CANCELLED) break;
const images = g.run.state.images;
if (end !== g.DONE) { tell("Generation failed. The log has ComfyUI's message."); break; }
if (end !== g.DONE) { // ComfyUI's own words: which node, and why
const e = g.run.state.error;
tell(e && e.message ? `Generation failed${e.node ? ` in ${e.node}` : ""}: ${e.message}` : "Generation failed. The log has ComfyUI's message.");
break;
}
if (!images.length) { tell("ComfyUI finished without a result. Try again."); break; }
await record(pid, unit, images[images.length - 1], frames, g.run.state.promptId); // the next shot may continue from this one: it must be on the clip first
snap.scene_renders = structuredClone(p.project.scene_renders || {}); // ...and in what the next shot reads
+18
View File
@@ -149,3 +149,21 @@ test("typing a value back while the change away from it is still saving puts it
await new Promise((r) => setTimeout(r, 20));
assert.equal(live[0].inputs.steps, 7);
});
test("a sampling setting that could not be saved is said in the window, not only on the Engine page", async () => {
const mod = { id: "sharpen", category: "sampling", settings: { enabled: { type: "bool", default: false, label: "Adjust detail" } } };
const ps = { slots: () => [], activeModules: () => [mod], useful: () => true, usefulness: () => () => true, currentValues: () => ({}), loading: () => false, loadError: () => null,
subscribe: () => () => {}, ensureLoaded: async () => {}, save: async () => {}, setModuleValue: async () => {}, settled: async () => true,
saveNotes: () => ["Could not save: ComfyUI refused this project's saved pipeline"] };
const host = document.createElement("div");
document.body.append(host);
hub.setup({ host, app: { pipeline: ps, api: { describeNodes: async () => ({ nodes: {} }) }, on: () => () => {}, actions: null } });
await new Promise((r) => setTimeout(r, 0));
host.querySelector("button").click();
await new Promise((r) => setTimeout(r, 0));
const before = document.body.textContent;
const box = [...document.querySelectorAll("input[type=checkbox], [role=switch]")].pop();
box.click();
await new Promise((r) => setTimeout(r, 10));
assert.match(document.body.textContent.replace(before, ""), /Could not save: ComfyUI refused/);
});
+7 -1
View File
@@ -49,7 +49,13 @@ export default {
if (ps.loading()) return body.set([c.hint.default({ text: "Loading…" })]);
if (ps.loadError()) return body.set([c.banner.warn({ text: `Could not load: ${ps.loadError()}` })]);
const mods = sampling(), values = ps.currentValues();
const change = async (id, name, value) => { await ps.setModuleValue(id, name, value); draw(); };
const change = async (id, name, value) => {
await ps.setModuleValue(id, name, value);
if (ps.settled) await ps.settled();
const why = (ps.saveNotes ? ps.saveNotes() : []).find((n) => /could not save/i.test(n));
if (why) c.toast.warn({ text: why }); // refused, or not saved: said here, not only on the Engine page
draw();
};
const rows = marked().map(inputRow);
const unread = marked().length && !rows.some(Boolean) && (unreadable || marked().every(({ slot }) => slot.node in specs)) ? [c.hint.default({ text: "Steps, sampler and scheduler could not be read from the sampler node: change them in Settings ▸ Models & Pipeline." })] : [];
body.set([...unread, ...rows, ...mods.flatMap((m) => [mods.length > 1 ? c.label.section({ text: m.title || m.id }) : null,
+7
View File
@@ -76,6 +76,13 @@ export const api = {
}
return body.prompt_id;
},
/** Whether ComfyUI's queue holds a run of this project (queued from any tab). Unreadable: false, the queue decides. */
async projectQueued(projectId) {
try {
const q = await (await fetch("/queue")).json();
return [...(q.queue_running || []), ...(q.queue_pending || [])].some((r) => (r[3] || {}).funpack_project_id === projectId);
} catch { return false; }
},
/** null while it is queued or running, else {videos} | {error} | {gone} | {retry} (a dropped reply is not a failed job). */
async upscaleResult(promptId) {
let entry = null;