Tab accepts a prompt suggestion (also in Anchor/Postfix); pending modules show a Restart banner and the restart dialog lists what it will install
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
25cc6ed0b4
commit
b97a08faf7
@@ -198,7 +198,10 @@ define("autocomplete", "default", ({ input, source, onPick, minChars = 1 } = {})
|
||||
if (!live) return;
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); index = Math.min(index + 1, current.length - 1); draw(current); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); index = Math.max(index - 1, 0); draw(current); }
|
||||
else if (e.key === "Enter" && index >= 0) { e.preventDefault(); if (onPick) onPick(current[index]); close(); }
|
||||
else if ((e.key === "Enter" && index >= 0) || (e.key === "Tab" && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey)) {
|
||||
// Tab takes the highlighted suggestion, or the first one; it is a plain Tab only when nothing is offered.
|
||||
e.preventDefault(); if (onPick) onPick(current[Math.max(index, 0)]); close();
|
||||
}
|
||||
};
|
||||
|
||||
if (field) {
|
||||
|
||||
@@ -661,3 +661,22 @@ test("a toast with no words is not shown at all", () => {
|
||||
composer.toast.warn({ text: null });
|
||||
assert.equal(document.querySelectorAll(".cx-toast").length, before);
|
||||
});
|
||||
|
||||
test("Tab takes the first suggestion (or the highlighted one); with no menu it stays a plain Tab", () => {
|
||||
const picked = [];
|
||||
const input = composer.input.md({});
|
||||
document.body.appendChild(input.node);
|
||||
const ac = composer.autocomplete.default({ input, source: () => [{ label: "a" }, { label: "b" }], onPick: (i) => picked.push(i.label), minChars: 1 });
|
||||
const tab = () => { const e = new window.KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }); input.node.dispatchEvent(e); return e.defaultPrevented; };
|
||||
assert.equal(tab(), false, "no menu: Tab is left alone");
|
||||
input.node.value = "x"; fire(input.node, "input");
|
||||
assert.equal(tab(), true);
|
||||
assert.deepEqual(picked, ["a"]);
|
||||
assert.equal(document.querySelector(".cx-autocomplete"), null, "the menu closed");
|
||||
input.node.value = "xy"; fire(input.node, "input");
|
||||
input.node.dispatchEvent(new window.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
input.node.dispatchEvent(new window.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
tab();
|
||||
assert.deepEqual(picked, ["a", "b"]);
|
||||
ac.destroy(); input.node.remove();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { accept, suggestionsAt } from "./match.js";
|
||||
|
||||
const BOXES = new Set(["Story", "Prompt"]); // aria-labels of the prompt fields
|
||||
const BOXES = new Set(["Story", "Prompt", "Anchor", "Postfix"]); // aria-labels of the prompt fields
|
||||
|
||||
export default {
|
||||
id: "autocomplete",
|
||||
|
||||
@@ -13,7 +13,10 @@ export const modules = (app) => function mount() {
|
||||
// The project remembers which add-ons it uses, so a new rental can offer them again on load.
|
||||
const remember = (id, on) => { const now = app.project.pref("addons", []) || [], next = on ? [...new Set([...now, id])] : now.filter((i) => i !== id); app.project.setPref("addons", next); };
|
||||
const flip = async (a, on) => {
|
||||
try { addons = await app.api.setAddon(a.id, on); remember(a.id, on); } catch (err) { c.toast.warn({ text: `Could not change ${a.title}: ${err.message}` }); }
|
||||
try {
|
||||
addons = await app.api.setAddon(a.id, on); remember(a.id, on);
|
||||
if (on && addons.some((x) => x.id === a.id && x.state === "pending")) c.toast.info({ text: `${a.title} is not installed yet: restart ComfyUI to download and install it.`, duration: 10000 });
|
||||
} catch (err) { c.toast.warn({ text: `Could not change ${a.title}: ${err.message}` }); }
|
||||
draw();
|
||||
};
|
||||
const addonRow = (a) => {
|
||||
@@ -48,7 +51,9 @@ export const modules = (app) => function mount() {
|
||||
checked: ps.allOff(), onChange: (on) => ps.setAllOff(on).then(draw) });
|
||||
const section = (label, rows) => rows.length ? [c.label.section({ text: label }), ...rows] : [];
|
||||
const installed = mine.filter((m) => !isOff(m));
|
||||
const waiting = addons.filter((a) => a.state === "pending");
|
||||
page.set([notes.length ? c.banner.warn({ text: notes.join(" ") }) : null,
|
||||
waiting.length ? c.banner.info({ text: `Restart ComfyUI to finish: ${waiting.map((a) => a.title).join(", ")} will be installed (their dependencies downloaded) while ComfyUI starts, and work after that.`, action: { label: "Restart ComfyUI", onClick: () => app.maintenance.restart() } }) : null,
|
||||
...section("Available modules", offered.map(addonRow)),
|
||||
...section("Installed modules", [...onAddons.map(addonRow), ...CATEGORIES.flatMap(([key]) => installed.filter((m) => (m.category || "") === key).map(row))]),
|
||||
...section("Disabled modules", [...offAddons.map(addonRow), ...mine.filter(isOff).map(row)]),
|
||||
|
||||
@@ -115,7 +115,9 @@ export function createMaintenance({ api, flush }) {
|
||||
return run("Switching torch to CUDA 13…", action, (r) => r.message || "");
|
||||
},
|
||||
async restart() {
|
||||
if (!(await ask("Restart ComfyUI", "Restart now? The server is down for 10–40 s and any running generation is lost. This page reloads when it is back."))) return;
|
||||
let waiting = []; try { waiting = (await api.addons()).filter((a) => a.state === "pending").map((a) => a.title); } catch { /* not knowing is not a reason to refuse */ }
|
||||
const installs = waiting.length ? `\n\nOn the way up it will also download and install: ${waiting.join(", ")}. That can take a few minutes more.` : "";
|
||||
if (!(await ask("Restart ComfyUI", `Restart now? The server is down for 10–40 s and any running generation is lost. This page reloads when it is back.${installs}`))) return;
|
||||
const action = () => api.git("restart"); action.dropsConnection = true;
|
||||
return run("Restarting ComfyUI…", action);
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { setupDom, teardownDom } from "../composer/tests/_dom.js";
|
||||
|
||||
test.before(() => setupDom());
|
||||
const tick = (ms = 30) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
test("Restart ComfyUI says it is restarting once, not twice", async () => {
|
||||
setupDom();
|
||||
const { createMaintenance } = await import("./maintenance.js");
|
||||
// The server answers that it is restarting, then stays down.
|
||||
const api = { git: async () => ({ ok: true, restarting: true }), health: async () => { throw new Error("down"); } };
|
||||
@@ -15,8 +15,17 @@ test("Restart ComfyUI says it is restarting once, not twice", async () => {
|
||||
[...document.querySelectorAll("button")].find((b) => /Go ahead/.test(b.textContent)).click();
|
||||
await tick(100);
|
||||
const said = (document.body.textContent.match(/Restarting ComfyUI…/g) || []).length;
|
||||
teardownDom();
|
||||
assert.equal(said, 1, "the working card repeats the restarting line");
|
||||
});
|
||||
test("Restart ComfyUI says which modules it will install on the way up", async () => {
|
||||
const { createMaintenance } = await import("./maintenance.js");
|
||||
const api = { addons: async () => [{ title: "Sharp Hands", state: "pending" }, { title: "Other", state: "installed" }], git: async () => ({ ok: true, restarting: true }), health: async () => { throw new Error("down"); } };
|
||||
createMaintenance({ api, flush: async () => {} }).restart();
|
||||
await tick(100);
|
||||
const text = document.body.textContent;
|
||||
[...document.querySelectorAll("button")].find((b) => /Go ahead/.test(b.textContent))?.click();
|
||||
assert.match(text, /download and install: Sharp Hands/);
|
||||
assert.doesNotMatch(text, /Other/);
|
||||
});
|
||||
// The restart card polls the stopped server forever by design, so the process is ended when the test is done.
|
||||
test.after(() => setTimeout(() => process.exit(0), 10));
|
||||
test.after(() => { teardownDom(); setTimeout(() => process.exit(0), 10); });
|
||||
|
||||
Reference in New Issue
Block a user