import { app } from "/scripts/app.js"; import { api } from "/scripts/api.js"; import { State, promptStates, pruneHistory, cardElements, saveStatesToLocalStorage } from "./state.js"; import { findImagesInOutputs, findTextsInOutputs } from "./utils.js"; export let renderDOMFn = () => {}; export let updateSidebarBadgeFn = () => {}; export function setUIDependencies(renderFn, badgeFn) { renderDOMFn = renderFn; updateSidebarBadgeFn = badgeFn; } export async function syncQueue() { try { const q = await api.getQueue(); const runningList = q.Running || q.queue_running || []; const pendingList = q.Pending || q.queue_pending || []; const pendingIds = new Set(); const normalizedPending = pendingList.map((p, idx) => { let pid = null, seq = idx; if (Array.isArray(p)) { seq = typeof p[0] === 'number' ? p[0] : idx; pid = p[1]; } else if (p && typeof p === "object") { pid = p.prompt_id || p.id || p.uuid; seq = typeof p.number === 'number' ? p.number : (typeof p.prompt_number === 'number' ? p.prompt_number : idx); } return { pid, seq, original: p }; }); normalizedPending.sort((a, b) => a.seq - b.seq); normalizedPending.forEach((item, index) => { const pid = item.pid; const number = index + 1; if (pid) { pendingIds.add(pid); if (!promptStates.has(pid)) { State.globalOrderCounter++; promptStates.set(pid, { pid: pid, status: "pending", images: [], progress: 0, queueNumber: number, progressText: `Pending... (#${number})`, timestamp: State.globalOrderCounter, workflow: app.graph.serialize() }); } else { const st = promptStates.get(pid); if (st.status === "pending") { st.queueNumber = number; st.progressText = `Pending... (#${number})`; } } } }); for (const [pid, state] of promptStates.entries()) { if (state.status === "pending" && !pendingIds.has(pid)) promptStates.delete(pid); } updateSidebarBadgeFn(pendingIds.size + (runningList.length > 0 ? 1 : 0)); renderDOMFn(); } catch (err) { console.error("Comfy Sidebar: Failed to sync queue state", err); } } const concludeRun = async (pid, statusStr) => { if (!pid || !promptStates.has(pid)) return; if (State.currentlyActivePromptId === pid) State.currentlyActivePromptId = null; const st = promptStates.get(pid); if (st._previewBlobUrl) { try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){} delete st._previewBlobUrl; } if (st._oldPreviewBlobUrl) { try { URL.revokeObjectURL(st._oldPreviewBlobUrl); } catch(e){} delete st._oldPreviewBlobUrl; } st.status = statusStr; st.progressText = ""; st.rendered = false; st.endTime = Date.now(); if (st.startTime) st.duration = (st.endTime - st.startTime) / 1000; try { const res = await fetch(`/history/${pid}`); const hItem = await res.json(); if (hItem && hItem[pid]) { if (!st.workflow) st.workflow = hItem[pid].extra_data?.extra_pnginfo?.workflow || null; st.nodeOutputs = hItem[pid].outputs; if (st.images.length === 0) st.images = findImagesInOutputs(hItem[pid].outputs, st.workflow); st.texts = findTextsInOutputs(hItem[pid].outputs, st.workflow); } } catch (err) {} pruneHistory(app); saveStatesToLocalStorage(); // Persist history state only when a job concludes syncQueue(); }; export function setupApiListeners() { api.addEventListener("status", syncQueue); api.addEventListener("execution_start", (e) => { if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) { const toDelete = []; for (const [p, s] of promptStates.entries()) { if (s.status === "cancelled" || s.status === "error") { toDelete.push(p); promptStates.delete(p); } } if (toDelete.length > 0) { api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }).catch(()=>{}); saveStatesToLocalStorage(); } } const pid = e.detail.prompt_id; State.currentlyActivePromptId = pid; const activeWorkspaceWorkflow = app.graph.serialize(); if (promptStates.has(pid)) { const st = promptStates.get(pid); st.status = "active"; st.progressText = "Sampling..."; st.workflow = activeWorkspaceWorkflow; st.rendered = false; st.startTime = Date.now(); st.duration = null; } else { State.globalOrderCounter++; promptStates.set(pid, { pid: pid, status: "active", images: [], progress: 0, progressText: "Sampling...", timestamp: State.globalOrderCounter, workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null }); } renderDOMFn(); syncQueue(); }); api.addEventListener("progress", (e) => { const pid = e.detail.prompt_id; if (pid && promptStates.has(pid)) { const st = promptStates.get(pid); st.progress = Math.round((e.detail.value / e.detail.max) * 100); // Direct fast DOM update for the progress bar without triggering a full history re-sort const cardObj = cardElements.get(pid); if (cardObj && cardObj.progressBar) { cardObj.progressBar.style.width = `${st.progress}%`; if (cardObj.statusText && cardObj.statusText.style.display !== "none") { const nodeName = st.activeNodeName ? (st.activeNodeName === "Finishing..." ? "Finishing..." : `[${st.activeNodeName}]`) : "Sampling..."; cardObj.statusText.textContent = `${nodeName} ${st.progress}%`; } } else { renderDOMFn(); } } }); api.addEventListener("executing", (e) => { const nodeId = e.detail; const showWorkingNode = app.ui.settings.getSettingValue("Comfy Sidebar.Show Working Node Name") ?? true; if (showWorkingNode && State.currentlyActivePromptId && promptStates.has(State.currentlyActivePromptId)) { const st = promptStates.get(State.currentlyActivePromptId); if (nodeId) { const node = app.graph.getNodeById(nodeId); st.activeNodeName = node ? (node.title || node.type) : `Node #${nodeId}`; } else { st.activeNodeName = "Finishing..."; } renderDOMFn(); } }); api.addEventListener("b_preview", (e) => { const activeTasks = Array.from(promptStates.values()).filter(t => t.status === "active"); if (activeTasks.length > 0) { const st = activeTasks[0]; const newBlobUrl = URL.createObjectURL(e.detail); // Retain old blob URL so ui.js can revoke it ONLY AFTER the image element finishes loading st._oldPreviewBlobUrl = st._previewBlobUrl; st._previewBlobUrl = newBlobUrl; st.images = [{ url: newBlobUrl }]; renderDOMFn(); } }); api.addEventListener("executed", (e) => { if (promptStates.has(e.detail.prompt_id)) { const st = promptStates.get(e.detail.prompt_id); const finalImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, st.workflow); if (finalImgs.length > 0) { st.images = finalImgs; } const finalTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, st.workflow); if (finalTexts.length > 0) { st.texts = finalTexts; } if (!st.nodeOutputs) st.nodeOutputs = {}; st.nodeOutputs[e.detail.node] = e.detail.output; renderDOMFn(); } }); api.addEventListener("execution_success", (e) => concludeRun(e.detail.prompt_id, "completed")); api.addEventListener("execution_error", (e) => concludeRun(e.detail.prompt_id, "error")); api.addEventListener("execution_interrupted", () => { Array.from(promptStates.values()).filter(t => t.status === "active").forEach(t => concludeRun(t.pid, "cancelled")); syncQueue(); }); } export async function initSessionAndHistory() { let backendSessionId = null; try { const res = await fetch("/classic-sidebar/session"); const data = await res.json(); backendSessionId = data.session_id; } catch (err) { console.warn("Comfy Sidebar: Custom session endpoint not found. Queue persistence will fall back to cache."); } const storedSessionId = localStorage.getItem("comfy_sidebar_backend_session_id"); if (backendSessionId && backendSessionId !== storedSessionId) { promptStates.clear(); cardElements.clear(); localStorage.removeItem("comfy_sidebar_prompt_states"); if (backendSessionId) localStorage.setItem("comfy_sidebar_backend_session_id", backendSessionId); } else if (!storedSessionId && backendSessionId) { localStorage.setItem("comfy_sidebar_backend_session_id", backendSessionId); } const historyData = await api.getHistory(); const ids = Object.keys(historyData).sort((a,b) => Number(a)-Number(b)); ids.forEach(id => { if (promptStates.has(id)) return; const workflow = historyData[id].extra_data?.extra_pnginfo?.workflow || null; const images = findImagesInOutputs(historyData[id].outputs, workflow); const texts = findTextsInOutputs(historyData[id].outputs, workflow); if (images.length === 0 && texts.length === 0) return; State.globalOrderCounter++; promptStates.set(id, { pid: id, status: "completed", images, texts, nodeOutputs: historyData[id].outputs, workflow: workflow, progressText: "", timestamp: State.globalOrderCounter, rendered: true }); }); pruneHistory(app); await syncQueue(); }