From c98537c15a4dfca49d87522ca026d0fec78b6edb Mon Sep 17 00:00:00 2001 From: Simon Cordt Date: Wed, 24 Sep 2025 19:35:13 +0200 Subject: [PATCH] feat: Add ability to rename jobs in the persistent queue --- server/database.py | 26 ++++++++++++++++++++++ server/queue_manager.py | 34 ++++++++++++++++++++++++++++ web/pqueue.js | 49 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) diff --git a/server/database.py b/server/database.py index e7a1406..fb3dedb 100644 --- a/server/database.py +++ b/server/database.py @@ -128,6 +128,32 @@ class QueueDatabase: with self._get_conn() as conn: conn.execute('UPDATE queue_items SET priority = ? WHERE prompt_id = ?', (new_priority, prompt_id)) + def update_job_name(self, prompt_id: str, new_name: str) -> bool: + """Update the human-friendly name in the stored workflow JSON. + + Attempts to set either workflow.name (nested) or top-level name, depending on structure. + Returns True on success, False if no such job. + """ + with self._get_conn() as conn: + cur = conn.execute('SELECT workflow FROM queue_items WHERE prompt_id = ? LIMIT 1', (prompt_id,)) + row = cur.fetchone() + if not row: + return False + wf_text = row['workflow'] + try: + wf = json.loads(wf_text) if isinstance(wf_text, str) else (wf_text or {}) + except Exception: + wf = {} + # Prefer nested workflow.name if object contains a workflow field + if isinstance(wf, dict): + if isinstance(wf.get('workflow'), dict): + wf['workflow']['name'] = str(new_name) + else: + wf['name'] = str(new_name) + updated = json.dumps(wf) if wf is not None else None + conn.execute('UPDATE queue_items SET workflow = ? WHERE prompt_id = ?', (updated, prompt_id)) + return True + def add_history( self, prompt_id: str, diff --git a/server/queue_manager.py b/server/queue_manager.py index e2bd1a1..ccd6be0 100644 --- a/server/queue_manager.py +++ b/server/queue_manager.py @@ -273,6 +273,7 @@ class RoutesHelper: web.post('/api/pqueue/reorder', manager._api_reorder), web.patch('/api/pqueue/priority', manager._api_priority), web.post('/api/pqueue/delete', manager._api_delete), + web.patch('/api/pqueue/rename', manager._api_rename), ] self.app.add_routes(routes) @@ -510,6 +511,39 @@ class PersistentQueueManager: logging.warning(f"PersistentQueue delete failed: {e}") return web.json_response({"ok": False, "error": str(e)}, status=400) + async def _api_rename(self, request: web.Request) -> web.Response: + try: + from server import PromptServer + body = await request.json() + prompt_id: str = body.get('prompt_id') + new_name: str = body.get('name') + if not prompt_id or new_name is None: + return web.json_response({"ok": False, "error": "prompt_id and name required"}, status=400) + # Persist to DB + ok = self.db.update_job_name(prompt_id, str(new_name)) + if not ok: + return web.json_response({"ok": False, "error": "job not found"}, status=404) + # Update in-memory queue copy to reflect quickly + q = PromptServer.instance.prompt_queue + with q.mutex: + for idx, item in enumerate(list(q.queue)): + try: + if item[1] == prompt_id: + # item = (number, prompt_id, prompt, extra_data, outputs_to_execute) + prompt = item[2] + if isinstance(prompt, dict): + if isinstance(prompt.get('workflow'), dict): + prompt['workflow']['name'] = str(new_name) + else: + prompt['name'] = str(new_name) + q.queue[idx] = (item[0], item[1], prompt, item[3], item[4]) + except Exception: + pass + return web.json_response({"ok": True}) + except Exception as e: + logging.warning(f"PersistentQueue rename failed: {e}") + return web.json_response({"ok": False, "error": str(e)}, status=400) + def _rebuild_queue_by_prompt_ids(self, ordered_prompt_ids: List[str]) -> None: from server import PromptServer q = PromptServer.instance.prompt_queue diff --git a/web/pqueue.js b/web/pqueue.js index afb38bf..e576fc6 100644 --- a/web/pqueue.js +++ b/web/pqueue.js @@ -27,6 +27,12 @@ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt_ids }), }), + rename: (prompt_id, name) => + fetch("/api/pqueue/rename", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ prompt_id, name }), + }), }; const Icons = { @@ -579,6 +585,49 @@ const status = UI.statusBadge(db.status || "pending", { subtle: true }); const primaryLabel = UI.buildWorkflowLabel(pid, item, db); + primaryLabel.contentEditable = "true"; + primaryLabel.spellcheck = false; + primaryLabel.dataset.id = pid; + primaryLabel.dataset.original = primaryLabel.textContent || ""; + primaryLabel.classList.add("pqueue-editable"); + const finishEdit = async (node, save) => { + try { + const id = node?.dataset?.id; + if (!id) return; + const original = node.dataset.original || ""; + const current = (node.textContent || "").trim(); + if (!save) { + node.textContent = original; + return; + } + if (current === original || !current) return; + await API.rename(id, current); + state.workflowNameCache.set(id, current); + setStatusMessage("Name updated"); + } catch (err) { + console.error("pqueue: rename failed", err); + state.error = err?.message || "Failed to rename"; + UI.updateToolbarStatus(); + } finally { + node.dataset.original = node.textContent || ""; + } + }; + primaryLabel.addEventListener("keydown", async (e) => { + e.stopPropagation(); + const target = e.currentTarget; + if (e.key === "Enter") { + e.preventDefault(); + await finishEdit(target, true); + target.blur(); + } else if (e.key === "Escape") { + e.preventDefault(); + await finishEdit(target, false); + target.blur(); + } + }); + primaryLabel.addEventListener("blur", async (e) => { + await finishEdit(e.currentTarget, true); + }); const line1 = UI.el("div", { class: "pqueue-pcard__line1" }, [ UI.el("div", { class: "pqueue-pcard__l1-left" }, [checkbox, primaryLabel]), UI.el("div", { class: "pqueue-pcard__l1-right" }, [UI.el("span", { class: "pqueue-muted", text: db.created_at ? Format.relative(db.created_at) : "—" })])