fix: Generate refuses, saying why, until the open project's pipeline is live; Sampler shows what will really run when a save fails; takes check the right pipeline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-05 14:14:34 +03:00
co-authored by Claude Opus 5.5
parent a9f6a7f4ad
commit b65ef15199
6 changed files with 42 additions and 5 deletions
+2 -2
View File
@@ -25,11 +25,11 @@ const on = (fn) => { heard.add(fn); return () => heard.delete(fn); };
const project = createProject({ keepConsistent: syncSeparated, onChange: () => say("change"), onOpen: () => say("open"), onError: (err) => console.warn(err), beforeSwitch: () => pipeline.settled() });
const pipeline = createPipelineState(api);
const generate = createGenerate({ pipeline });
linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "open") fn(); }), say: (text) => c.toast.warn({ text }) });
const link = linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "open") fn(); }), say: (text) => c.toast.warn({ text }) });
const frame = build(document.getElementById("app"));
const selection = createSelection({ project });
selection.on(() => say("select"));
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, pipelineOwned: link.owns, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
const settings = createSettings({ menubar: hostFor("menubar.menus") });
app.openSettings = (id) => settings.open(id);
const result = await loadFeatures(paths, { app });
+10
View File
@@ -83,3 +83,13 @@ test("a batch with no seed input says so and makes nothing", async () => {
assert.equal(queued, 0);
assert.match(document.body.textContent, /no seed input/);
});
test("Generate refuses, saying why, while the open project's pipeline is not the live one", async () => {
let queued = 0;
const { host, app } = rig("idle", { generate: async () => { queued += 1; return true; } });
app.pipelineOwned = () => false;
host.querySelector("button").click();
await new Promise((r) => setTimeout(r, 20));
assert.equal(queued, 0);
assert.match(document.body.textContent, /pipeline is not loaded yet/);
});
+2 -1
View File
@@ -51,7 +51,6 @@ export default {
// `times` > 1: the same shots again, only the seed differing (the prompt's random picks are drawn once), so the takes can be compared.
async function runUnits(units, times = 1) {
if (busy) { if (times > 1) tell("A run is already going: wait for it, or Stop it."); return; }
if (times > 1 && !rolesAt(app.pipeline.slots(), "generation.seed").length) return tell("This pipeline has no seed input the app controls, so the takes would all come out the same.");
busy = true; stopped = false; draw();
const pid = p.project.id;
let made = 0;
@@ -59,6 +58,8 @@ export default {
try {
// 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: this project's pipeline is not loaded yet (ComfyUI did not answer). It goes in as soon as ComfyUI answers."); return; }
if (times > 1 && !rolesAt(app.pipeline.slots(), "generation.seed").length) { tell("This pipeline has no seed input the app controls, so the takes would all come out the same."); return; }
const snap = times > 1 ? structuredClone(p.project) : null; // takes differ in the seed alone: later edits to the project do not reach them
const drawn = new Map();
const same = times > 1 ? (body) => { const key = JSON.stringify(body); if (!drawn.has(key)) drawn.set(key, expand(body)); return drawn.get(key); } : expand;
+19
View File
@@ -64,3 +64,22 @@ test("typing a value back to what the window opened with is saved, though the wi
}
assert.deepStrictEqual(saved, [20, 7]);
});
test("a value that could not be saved is not left on screen: the window shows what will run and says why", async () => {
const live = [{ id: "sampler", node: "S", inputs: { steps: 7 }, roles: [{ at: "generation.sampling", input: "steps", label: "Steps" }] }];
const ps = { slots: () => live, activeModules: () => [], useful: () => true, usefulness: () => () => true, currentValues: () => ({}), loading: () => false, loadError: () => null,
subscribe: () => () => {}, ensureLoaded: async () => {}, save: async () => {}, saveNotes: () => ["Could not save: down"] };
const host = document.createElement("div");
document.body.append(host);
hub.setup({ host, app: { pipeline: ps, api: { describeNodes: async () => ({ nodes: { S: { widgets: [{ name: "steps", type: "INT", min: 1, max: 100 }] } } }) }, on: () => () => {}, actions: null } });
await new Promise((r) => setTimeout(r, 0));
host.querySelector("button").click();
await new Promise((r) => setTimeout(r, 0));
let input = [...document.querySelectorAll("input[type=number]")].pop();
input.value = "25";
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
await new Promise((r) => setTimeout(r, 0));
input = [...document.querySelectorAll("input[type=number]")].pop();
assert.equal(input.value, "7");
assert.match(document.body.textContent, /Could not save: down/);
});
+7 -1
View File
@@ -24,7 +24,13 @@ export default {
if (!w) return null;
const set = async (v) => { // compared with the slot as it is NOW: the one drawn may be several saves old
const now = (ps.slots() || []).find((s) => s.id === slot.id) || slot;
if (JSON.stringify((now.inputs || {})[role.input]) !== JSON.stringify(v)) await ps.save({ inputs: { [slot.id]: { [role.input]: v } } });
if (JSON.stringify((now.inputs || {})[role.input]) === JSON.stringify(v)) return;
await ps.save({ inputs: { [slot.id]: { [role.input]: v } } });
const after = (ps.slots() || []).find((s) => s.id === slot.id) || {};
if (JSON.stringify((after.inputs || {})[role.input]) !== JSON.stringify(v)) { // not saved: show what will really run, and why
c.toast.warn({ text: [...(ps.saveNotes ? ps.saveNotes() : [])].find((n) => /could not save/i.test(n)) || `${role.label || role.input} was not saved.` });
draw();
}
};
return c.settingsRow.default({ label: role.label || w.name, hint: w.tooltip || "", control: widgetControl({ ...w, name: role.label || w.name }, (slot.inputs || {})[role.input] ?? w.default, set) });
};
+2 -1
View File
@@ -52,5 +52,6 @@ export function linkPipeline({ project, pipeline, onOpen, say = () => {} }) {
store(project.project.models || {}, slots);
});
onOpen(() => { retries = 0; return adopt(); }); // each project opened gets its own retries
return { adopt };
// Whether the live pipeline is the open project's (false while it is going in, or ComfyUI has not answered).
return { adopt, owns: () => !!project.project && owner === project.project.id };
}