From ce60e4d9adc42bb3ec114be979ffb0769614fc3a Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:24:55 +0300 Subject: [PATCH] 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 --- app/ui/features/generate/hub.test.js | 18 ++++++++++++++++++ app/ui/features/generate/index.js | 8 +++++++- app/ui/features/sampler/hub.test.js | 18 ++++++++++++++++++ app/ui/features/sampler/index.js | 8 +++++++- app/ui/shell/api.js | 7 +++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/app/ui/features/generate/hub.test.js b/app/ui/features/generate/hub.test.js index 45609f8a..353ee2ce 100644 --- a/app/ui/features/generate/hub.test.js +++ b/app/ui/features/generate/hub.test.js @@ -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/); +}); diff --git a/app/ui/features/generate/index.js b/app/ui/features/generate/index.js index f677c72b..8d46620c 100644 --- a/app/ui/features/generate/index.js +++ b/app/ui/features/generate/index.js @@ -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 diff --git a/app/ui/features/sampler/hub.test.js b/app/ui/features/sampler/hub.test.js index cbf3d7ab..3b09ced3 100644 --- a/app/ui/features/sampler/hub.test.js +++ b/app/ui/features/sampler/hub.test.js @@ -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/); +}); diff --git a/app/ui/features/sampler/index.js b/app/ui/features/sampler/index.js index 0922378d..bf883293 100644 --- a/app/ui/features/sampler/index.js +++ b/app/ui/features/sampler/index.js @@ -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, diff --git a/app/ui/shell/api.js b/app/ui/shell/api.js index 361f515f..fa181e6b 100644 --- a/app/ui/shell/api.js +++ b/app/ui/shell/api.js @@ -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;