From 175c1a7643281fad2fbb00dfa8385f476d2add78 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:20:20 +0300 Subject: [PATCH] fix(ui): maintenance refuses when edits are unsaved and reports a blocked or absent restart; Models/Engine keep typing, show save errors, multiline text; export keeps its project name and picked clips Co-Authored-By: Claude Sonnet 5.5 --- app/ui/boot.js | 2 +- app/ui/features/render/clips.js | 2 +- app/ui/features/render/clips.test.js | 4 ++++ app/ui/features/render/selected.js | 8 ++++---- app/ui/features/settings/engine.js | 4 +++- app/ui/features/settings/models.js | 13 ++++++++++--- app/ui/features/settings/modules.js | 5 +++-- app/ui/shell/maintenance.js | 10 ++++++++-- app/ui/shell/project.js | 1 + 9 files changed, 35 insertions(+), 14 deletions(-) diff --git a/app/ui/boot.js b/app/ui/boot.js index 39dca5bb..8fff8c27 100644 --- a/app/ui/boot.js +++ b/app/ui/boot.js @@ -28,7 +28,7 @@ linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "o const frame = build(document.getElementById("app")); const selection = createSelection({ project }); selection.on(() => say("select")); -const app = { project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, dock: createDock(), maintenance: createMaintenance({ api, flush: () => project.flush() }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } }; +const app = { project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, 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") }); const result = await loadFeatures(paths, { app }); settings.addButton(); diff --git a/app/ui/features/render/clips.js b/app/ui/features/render/clips.js index c1202547..0a7eccfb 100644 --- a/app/ui/features/render/clips.js +++ b/app/ui/features/render/clips.js @@ -7,7 +7,7 @@ export function clipSpecs(p, only) { // `only`: a Set of scene ids to keep (th const clips = []; let missing = 0; for (const seg of segments(p)) { - if (seg.kind !== "scene" || (seg.scene.excluded && !seg.scene.removed_from_plan)) continue; // removed from the plan still plays + if (seg.kind !== "scene" || (!only && seg.scene.excluded && !seg.scene.removed_from_plan)) continue; // removed from the plan still plays; a clip picked by hand is wanted even if left out of the full run if (only && !only.has(seg.scene.id)) continue; const sc = seg.scene, r = renders[sc.id], base = { scene_id: sc.id, dur: seg.dur, fps: effFps(sc, p), gap_after: Math.max(0, +sc.gap_after_sec || 0) }; if (isVideoClip(sc)) { diff --git a/app/ui/features/render/clips.test.js b/app/ui/features/render/clips.test.js index 11982b60..13d1e446 100644 --- a/app/ui/features/render/clips.test.js +++ b/app/ui/features/render/clips.test.js @@ -20,3 +20,7 @@ test("only the picked clips are kept, still in timeline order, and unpicked ones assert.deepEqual(clips.map((c) => c.scene_id), ["a", "v"]); assert.equal(missing, 0); }); + +test("a clip picked by hand is exported even when it is excluded from the full run", () => { + assert.deepEqual(clipSpecs(p, new Set(["c"])).clips.map((c) => c.scene_id), ["c"]); +}); diff --git a/app/ui/features/render/selected.js b/app/ui/features/render/selected.js index d36acf9d..cc791a12 100644 --- a/app/ui/features/render/selected.js +++ b/app/ui/features/render/selected.js @@ -31,15 +31,15 @@ export default { try { await work(); } catch (err) { tell(err.message); } finally { busy = false; draw(); } }; const exportIt = run(async () => { - const media = await join(); + const name = p.project.name, media = await join(); // named for the project it was started in if (!media) return; - const link = Object.assign(document.createElement("a"), { href: viewUrl(media), download: `${p.project.name}.mp4` }); + const link = Object.assign(document.createElement("a"), { href: viewUrl(media), download: `${name}.mp4` }); link.click(); }); const toBin = run(async () => { - const media = await join(); + const name = p.project.name, media = await join(); if (!media) return; - const { media: entry } = await call("POST", "/api/import-clip", { clip: media, name: `${p.project.name}.mp4` }); + const { media: entry } = await call("POST", "/api/import-clip", { clip: media, name: `${name}.mp4` }); app.say("media"); c.toast.good({ text: `Saved to the media bin as ${entry.name}.` }); }); diff --git a/app/ui/features/settings/engine.js b/app/ui/features/settings/engine.js index ae066fbe..34d02148 100644 --- a/app/ui/features/settings/engine.js +++ b/app/ui/features/settings/engine.js @@ -27,6 +27,8 @@ export const engine = (app) => function mount() { } draw(); ps.ensureLoaded().then(draw); - const off = ps.subscribe(draw); + const typing = () => page.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName); + page.node.addEventListener("focusout", () => setTimeout(() => { if (!typing()) draw(); })); + const off = ps.subscribe(() => { if (!typing()) draw(); }); return { node: page.node, destroy: () => { off(); page.node.remove(); } }; }; diff --git a/app/ui/features/settings/models.js b/app/ui/features/settings/models.js index 1b4f1b96..ee569e28 100644 --- a/app/ui/features/settings/models.js +++ b/app/ui/features/settings/models.js @@ -14,7 +14,7 @@ const widgetControl = (w, current, set) => { return c.number.md({ label, value: current ?? w.default ?? 0, min: w.min, max: w.max, step: w.step ?? (w.type === "INT" ? 1 : undefined), precision: w.type === "INT" ? 0 : undefined, onChange: set }); } - return c.input.md({ label, value: current ?? "", onCommit: set }); + return w.multiline ? c.textarea.md({ label, value: current ?? "", rows: 4, onCommit: set }) : c.input.md({ label, value: current ?? "", onCommit: set }); }; export const models = (app) => function mount() { @@ -30,7 +30,11 @@ export const models = (app) => function mount() { try { Object.assign(specs, (await app.api.describeNodes(missing)).nodes || {}); } catch (err) { note = err.message; } missing.forEach((n) => { if (!(n in specs)) specs[n] = null; }); } - const set = (slot, name) => async (value) => { await ps.save({ inputs: { [slot.id]: { [name]: value } } }); note = (ps.refused() || []).join(" "); draw(); }; + const set = (slot, name) => async (value) => { + if (JSON.stringify((slot.inputs || {})[name]) === JSON.stringify(value)) return; // a blur that changed nothing saves nothing + await ps.save({ inputs: { [slot.id]: { [name]: value } } }); + note = [...(ps.refused() || []), ...ps.saveNotes()].join(" "); draw(); + }; const groups = []; slots.forEach((s) => { const g = s.group || "Other"; (groups.find((x) => x[0] === g) || (groups.push([g, []]), groups[groups.length - 1]))[1].push(s); }); page.set([ @@ -45,8 +49,11 @@ export const models = (app) => function mount() { })]), ].filter(Boolean)); } + // A redraw while a box has focus would eat what is being typed: it waits for the focus to leave. + const typing = () => page.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName); + page.node.addEventListener("focusout", () => setTimeout(() => { if (!typing()) draw(); })); ps.ensureLoaded().then(draw); - off = ps.subscribe(() => draw()); + off = ps.subscribe(() => { if (!typing()) draw(); }); const handle = { node: page.node, destroy: () => { off && off(); page.node.remove(); } }; return handle; }; diff --git a/app/ui/features/settings/modules.js b/app/ui/features/settings/modules.js index 38ae1e8a..1e7e70bc 100644 --- a/app/ui/features/settings/modules.js +++ b/app/ui/features/settings/modules.js @@ -26,8 +26,9 @@ export const modules = (app) => function mount() { return c.toggle.default({ label: name(m), hint: ps.isOff(m.id) ? "Off for this project" : undefined, checked: !ps.isOff(m.id), onChange: (on) => ps.setOff(m.id, !on).then(draw) }); }; - page.set([c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }), - ...CATEGORIES.flatMap(([key, label]) => { const list = mine.filter((m) => (m.category || "") === key); return list.length ? [c.label.section({ text: label }), ...list.map(row)] : []; })]); + const notes = ps.saveNotes(); + page.set([notes.length ? c.banner.warn({ text: notes.join(" ") }) : null, c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }), + ...CATEGORIES.flatMap(([key, label]) => { const list = mine.filter((m) => (m.category || "") === key); return list.length ? [c.label.section({ text: label }), ...list.map(row)] : []; })].filter(Boolean)); } draw(); ps.ensureLoaded().then(() => ps.refreshControl()).then(draw); diff --git a/app/ui/shell/maintenance.js b/app/ui/shell/maintenance.js index 90c9855a..9b808576 100644 --- a/app/ui/shell/maintenance.js +++ b/app/ui/shell/maintenance.js @@ -27,12 +27,18 @@ export function createMaintenance({ api, flush }) { } } async function run(message, action, said) { - await flush(); + try { await flush(); } catch (err) { return tell(err.message); } const text = c.text.md({ text: message }); const card = c.modal.generic({ title: "Working", size: "sm", closeOnOutside: false, body: c.region.stack({ gap: "md", children: [c.progress.indeterminate({ label: "Working" }), text] }) }); card.setText = (t) => text.setText ? text.setText(t) : (text.node.textContent = t); try { const res = await action(); + if (res && res.restarting === false) { // nothing restarted: say what is true, and stay on the page + card.close(); + return c.modal.dialogue({ title: "Not restarted", message: `${res.blocked || (said ? said(res) : "Nothing changed.")} ${res.blocked ? "Restart ComfyUI yourself when the generation has finished." : ""}`.trim(), confirmLabel: "OK", cancelLabel: "Close" }).result; + } + const bad = res && res.requirements && res.requirements.ran && !res.requirements.ok; + if (bad) { await c.modal.dialogue({ title: "Dependencies failed", message: `${res.requirements.detail || ""}\n\nFunPack was updated, but installing its dependencies failed: it may not load until they are installed by hand. ComfyUI will restart now.`, confirmLabel: "Restart", cancelLabel: "Restart" }).result; } card.setText(`${said ? said(res) : message}\nRestarting ComfyUI…`); } catch (err) { if (action.dropsConnection) { /* the server went down as it restarted: expected */ } else { card.close(); return tell(err.message); } @@ -47,7 +53,7 @@ export function createMaintenance({ api, flush }) { let gs; try { gs = await api.gitFull(); } catch { gs = null; } if (!gs || !gs.ok) return { ok: false, update: "Git unavailable for this install", branch: "Git unavailable for this install", rollback: "Git unavailable for this install" }; return { ok: true, - update: gs.dirty ? "Local changes — commit first" : gs.behind > 0 ? `${gs.behind} commit(s) behind origin/${gs.branch}` : `origin/${gs.branch} up to date`, + update: gs.fetch_ok === false ? "Could not reach origin: update status unknown" : gs.dirty ? "Local changes — commit first" : gs.behind > 0 ? `${gs.behind} commit(s) behind origin/${gs.branch}` : `origin/${gs.branch} up to date`, branch: `On ${gs.branch}${gs.dirty ? " · local changes — commit first" : " · pick another"}`, rollbackOk: Boolean(gs.rollback_target), rollback: gs.rollback_target ? `Undo last update — back to ${String(gs.rollback_target.commit).slice(0, 8)}` : "Nothing to undo yet" }; diff --git a/app/ui/shell/project.js b/app/ui/shell/project.js index e7e5a1c6..c6c67f82 100644 --- a/app/ui/shell/project.js +++ b/app/ui/shell/project.js @@ -169,6 +169,7 @@ export function createProject({ onChange, onError, onOpen } = {}) { return { get project() { return project; }, + get unsaved() { return dirty; }, get scenes() { return project ? project.scenes || [] : []; }, get selectedId() { return selected; }, get selected() { return sceneAt(selected); },