Files
co5dt-ComfyUI-Persistent-Queue/web/pqueue.js
T

743 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Minimal frontend extension for ComfyUI Persistent Queue
// Loaded automatically from WEB_DIRECTORY by ComfyUI and served at /extensions/<name>/pqueue.js
(function() {
const API = {
getQueue: () => fetch('/api/pqueue').then(r => r.json()),
getHistory: (limit=50) => fetch(`/api/pqueue/history?limit=${limit}`).then(r => r.json()),
pause: () => fetch('/api/pqueue/pause', { method: 'POST' }),
resume: () => fetch('/api/pqueue/resume', { method: 'POST' }),
reorder: (order) => fetch('/api/pqueue/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order }) }),
setPriority: (prompt_id, priority) => fetch('/api/pqueue/priority', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt_id, priority }) }),
del: (prompt_ids) => fetch('/api/pqueue/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt_ids }) }),
};
const state = {
paused: false,
queue_running: [],
queue_pending: [],
db_pending: [],
history: [],
running_progress: {},
container: null,
listPending: null,
selectedPending: new Set(),
};
// Polling control (fallback only)
let _pqueuePollIntervalId = null;
let _pqueueFocusListener = null;
const Progress = {
computeAggregateProgress(nodes) {
let totalMax = 0;
let totalVal = 0;
Object.values(nodes ?? {}).forEach((st) => {
const mv = Math.max(1, Number(st?.max ?? 1));
const vv = Math.max(0, Math.min(Number(st?.value ?? 0), mv));
totalMax += mv;
totalVal += vv;
});
return totalMax > 0 ? Math.max(0, Math.min(1, totalVal / totalMax)) : 0;
},
};
const UI = {
el(tag, attrs = {}, children = []) {
const e = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (k === 'class') {
e.className = v;
} else if (k === 'text') {
e.textContent = v;
} else {
e.setAttribute(k, v);
}
}
for (const c of children) {
e.appendChild(c);
}
return e;
},
createButton({ id, text, icon, classes, onClick }) {
const button = UI.el('button', { id, class: `p-button p-component ${classes}` });
if (icon) {
button.appendChild(UI.el('span', { class: `p-button-icon pi ${icon}` }));
}
if (text) {
button.appendChild(UI.el('span', { class: 'p-button-label', text }));
}
if (onClick) {
button.onclick = onClick;
}
return button;
},
createCard(title, content) {
return UI.el('div', { class: 'p-card pqueue-card' }, [
UI.el('div', { class: 'p-card-body' }, [
UI.el('div', { class: 'p-card-title', text: title }),
content,
]),
]);
},
updateProgressBarsFromState() {
if (!state.container) return;
Object.entries(state.running_progress).forEach(([pid, f]) => {
const bar = state.container.querySelector(`.pqueue-item[data-id="${pid}"] .pqueue-progress-bar`);
if (bar) bar.style.width = `${(Number(f) * 100).toFixed(2)}%`;
});
},
ensureThemeLink() {
if (!document.getElementById('pqueue-style-link')) {
const link = document.createElement('link');
link.id = 'pqueue-style-link';
link.rel = 'stylesheet';
link.href = '/extensions/ComfyUI-PersistentQueue/css/queue_style.css';
document.head.appendChild(link);
}
},
renderToolbar() {
const pauseBtn = UI.createButton({
id: 'pqueue-toggle',
text: state.paused ? 'Resume' : 'Pause',
icon: state.paused ? 'pi-play' : 'pi-pause',
classes: state.paused ? 'p-button-success' : 'p-button-warning',
});
const clearBtn = UI.createButton({
id: 'pqueue-clear',
text: 'Clear Pending',
icon: 'pi-stop',
classes: 'p-button-text p-button-danger',
});
return UI.el('div', { class: 'pqueue-toolbar' }, [pauseBtn, clearBtn]);
},
renderRunningSection() {
const runningList = UI.el('ul', { class: 'pqueue-list' });
if (!state.queue_running.length) {
runningList.appendChild(UI.el('li', { class: 'pqueue-empty', text: 'No running job' }));
} else {
state.queue_running.forEach(item => {
const pid = item[1];
const frac = Math.max(0, Math.min(1, Number(state.running_progress?.[pid]) || 0));
const progressBar = UI.el('div', { class: 'pqueue-progress-bar' });
progressBar.style.width = `${(frac * 100).toFixed(2)}%`;
const li = UI.el('li', { class: 'pqueue-item', 'data-id': pid }, [
UI.el('div', { class: 'pqueue-left' }, [
UI.el('div', { class: 'pqueue-meta' }, [
UI.el('i', { class: 'pi pi-spin pi-spinner' }),
UI.el('span', { text: pid })
]),
UI.el('div', { class: 'pqueue-progress-wrap' }, [progressBar]),
]),
UI.el('div', { class: 'pqueue-actions' }, [])
]);
runningList.appendChild(li);
});
}
return UI.createCard('Running', runningList);
},
renderPendingSection() {
const pendingList = UI.el('ul', { id: 'pqueue-pending', class: 'pqueue-list' });
if (!state.queue_pending.length) {
pendingList.appendChild(UI.el('li', { class: 'pqueue-empty', text: 'No pending items' }));
} else {
state.queue_pending.forEach(item => {
const checked = state.selectedPending.has(item[1]);
const checkbox = UI.el('input', { type: 'checkbox', class: 'p-checkbox-input pqueue-select' });
if (checked) checkbox.checked = true;
const priorityInput = UI.el('input', { type: 'number', value: '0', class: 'p-inputtext pqueue-priority', min: '0', style: 'width:5rem' });
const savePriorityBtn = UI.createButton({
icon: 'pi-sort-amount-up-alt',
classes: 'p-button-text pqueue-priority-save',
});
const deleteBtn = UI.createButton({
icon: 'pi-trash',
classes: 'p-button-text p-button-danger pqueue-delete',
});
const li = UI.el('li', { draggable: 'true', 'data-id': item[1], class: 'pqueue-item' }, [
UI.el('div', { class: 'pqueue-meta' }, [
UI.el('i', { class: 'pi pi-bars', style: 'cursor:grab' }),
checkbox,
UI.el('span', { text: item[1] })
]),
UI.el('div', { class: 'pqueue-actions' }, [
priorityInput,
savePriorityBtn,
deleteBtn,
])
]);
pendingList.appendChild(li);
});
}
return UI.createCard(`Pending (${state.queue_pending.length})`, pendingList);
},
renderHistorySection() {
const historyList = UI.el('ul', { class: 'pqueue-list' });
if (!state.history.length) {
historyList.appendChild(UI.el('li', { class: 'pqueue-empty', text: 'No history' }));
} else {
state.history.forEach(row => {
const ok = (row.status ?? 'success') === 'success';
const thumbs = UI.buildResultThumbs(row);
const time = UI.buildDuration(row);
const li = UI.el('li', { class: 'pqueue-item' }, [
UI.el('div', { class: 'pqueue-meta' }, [
UI.el('i', { class: `pi ${ok ? 'pi-check pqueue-status-success' : 'pi-times pqueue-status-error'}` }),
(time ? UI.el('span', { class: 'task-time', text: time }) : UI.el('span', {}))
]),
UI.el('div', { class: 'pqueue-thumbs-grid' }, thumbs.children.length ? Array.from(thumbs.children) : [])
]);
historyList.appendChild(li);
});
}
return UI.createCard('History', historyList);
},
render() {
if (!state.container) return;
UI.ensureThemeLink();
state.container.innerHTML = '';
const root = UI.el('div', { class: 'pqueue-root' });
const toolbar = UI.renderToolbar();
const sections = UI.el('div', { class: 'pqueue-sections' });
sections.appendChild(UI.renderRunningSection());
sections.appendChild(UI.el('div', { class: 'pqueue-divider' }));
sections.appendChild(UI.renderPendingSection());
sections.appendChild(UI.el('div', { class: 'pqueue-divider' }));
sections.appendChild(UI.renderHistorySection());
root.appendChild(toolbar);
root.appendChild(sections);
state.container.appendChild(root);
Events.init();
UI.updateProgressBarsFromState();
},
getThumbPlaceholderMeta(row) {
const status = String(row?.status ?? '').toLowerCase();
if (['interrupted', 'cancelled', 'canceled', 'stopped'].includes(status)) {
return { text: 'Interrupted', iconClass: 'pi-stop-circle', variant: 'interrupted' };
}
if (['error', 'failed', 'failure'].includes(status)) {
return { text: 'Failed', iconClass: 'pi-times-circle', variant: 'failed' };
}
return { text: 'Preview unavailable', iconClass: 'pi-image-off', variant: 'empty' };
},
createThumbPlaceholder(options = {}) {
const { text, iconClass, variant } = options ?? {};
const classes = ['pqueue-thumb-placeholder'];
if (variant) classes.push(`pqueue-thumb-placeholder--${variant}`);
const placeholder = UI.el('div', { class: classes.join(' ') });
const icon = UI.el('i', { class: `pi ${iconClass || 'pi-image-off'}` });
const label = UI.el('span', { class: 'pqueue-thumb-placeholder-text', text: text || 'Preview unavailable' });
placeholder.appendChild(icon);
placeholder.appendChild(label);
return placeholder;
},
buildResultThumbs(row) {
try {
const container = UI.el('div', { class: 'flex gap-1 flex-wrap' });
const placeholderMeta = UI.getThumbPlaceholderMeta(row);
const ensurePlaceholder = () => {
const wrapClasses = ['pqueue-thumb-wrap', 'pqueue-thumb-wrap-placeholder'];
if (placeholderMeta?.variant) wrapClasses.push(`pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}`);
const wrap = UI.el('div', { class: wrapClasses.join(' ') });
wrap.appendChild(UI.createThumbPlaceholder(placeholderMeta));
container.appendChild(wrap);
};
const attachFallbackHandler = (thumbWrap, img, images = []) => {
img.addEventListener('error', () => {
thumbWrap.classList.add('pqueue-thumb-wrap-placeholder');
if (placeholderMeta?.variant) thumbWrap.classList.add(`pqueue-thumb-wrap-placeholder--${placeholderMeta.variant}`);
thumbWrap.replaceChildren(UI.createThumbPlaceholder(placeholderMeta));
if (!images.length) thumbWrap.onclick = null;
}, { once: true });
};
// Prefer stored DB thumbnail if available; bust cache with timestamp to avoid race
if (row.id) {
const galleryImages = UI.extractImages(row);
const url = new URL(`/api/pqueue/history/thumb/${row.id}`, window.location.origin);
url.searchParams.set('_', String(Date.now()));
const thumbWrap = UI.el('div', { class: 'pqueue-thumb-wrap' });
const img = UI.el('img', { src: url.href, class: 'pqueue-thumb', title: `history-${row.id}` });
attachFallbackHandler(thumbWrap, img, galleryImages);
thumbWrap.appendChild(img);
const imgCount = UI.countImages(row);
if (imgCount > 1) {
const badge = UI.el('div', { class: 'pqueue-batch-badge', text: `${imgCount}` });
thumbWrap.appendChild(badge);
}
if (galleryImages.length) {
thumbWrap.onclick = () => UI.openGallery(galleryImages, 0, row.prompt_id);
} else {
thumbWrap.onclick = null;
}
container.appendChild(thumbWrap);
}
// Fallback to live preview route if DB thumb missing
if (!container.children.length) {
const images = UI.extractImages(row);
if (images.length) {
const first = images[0];
const url = UI.buildPreviewUrl(first, row.prompt_id);
const thumbWrap = UI.el('div', { class: 'pqueue-thumb-wrap' });
const img = UI.el('img', { src: url.href, class: 'pqueue-thumb', title: first.filename });
attachFallbackHandler(thumbWrap, img, images);
thumbWrap.appendChild(img);
if (images.length > 1) {
const badge = UI.el('div', { class: 'pqueue-batch-badge', text: `${images.length}` });
thumbWrap.appendChild(badge);
}
thumbWrap.onclick = () => UI.openGallery(images, 0, row.prompt_id);
container.appendChild(thumbWrap);
}
}
if (!container.children.length) ensurePlaceholder();
return container;
} catch(e) { return UI.el('span', {}); }
},
countImages(row) {
try {
return UI.extractImages(row).length;
} catch(e) { return 0; }
},
buildPreviewUrl(imgDesc, pid) {
const url = new URL('/api/pqueue/preview', window.location.origin);
url.searchParams.set('filename', imgDesc.filename);
url.searchParams.set('type', imgDesc.type);
if (imgDesc.subfolder) url.searchParams.set('subfolder', imgDesc.subfolder);
url.searchParams.set('preview', 'webp;50');
if (pid) url.searchParams.set('pid', pid);
return url;
},
// Collect normalized image descriptors for a row
extractImages(row) {
try {
let outputs = row.outputs ?? {};
if (typeof outputs === 'string') {
try { outputs = JSON.parse(outputs); } catch(e) { outputs = {}; }
}
const images = [];
Object.values(outputs).forEach(v => {
if (v && typeof v === 'object') {
const imgs = v.images ?? v.ui?.images ?? [];
if (Array.isArray(imgs)) imgs.forEach(i => {
const filename = i.filename ?? i.name;
if (filename) images.push({ filename, type: i.type ?? 'output', subfolder: i.subfolder ?? '' });
});
} else if (Array.isArray(v)) {
v.forEach(i => {
const filename = i?.filename ?? i?.name;
if (filename) images.push({ filename, type: i.type ?? 'output', subfolder: i.subfolder ?? '' });
});
}
});
return images;
} catch(e) { return []; }
},
buildDuration(row) {
try {
const dur = Number(row.duration_seconds);
if (!Number.isNaN(dur) && dur > 0) {
return `${dur.toFixed(2)}s`;
}
const started = row.created_at ?? row.started_at;
const completed = row.completed_at;
if (!started || !completed) return '';
const t1 = new Date(started).getTime();
const t2 = new Date(completed).getTime();
if (!isFinite(t1) || !isFinite(t2)) return '';
const sec = Math.max(0, (t2 - t1) / 1000);
return `${sec.toFixed(2)}s`;
} catch(e) { return ''; }
},
// Lightbox-style gallery for image batches
openGallery(images, startIndex = 0, pid = null) {
try {
UI.ensureGalleryElements();
UI._galleryState.images = images || [];
UI._galleryState.pid = pid;
UI._galleryState.index = Math.max(0, Math.min(startIndex, UI._galleryState.images.length - 1));
UI._galleryOverlay.style.display = 'flex';
UI.showGalleryImage(UI._galleryState.index);
UI._bindGalleryKeys(true);
} catch (e) { /* noop */ }
},
closeGallery() {
if (UI._galleryOverlay) UI._galleryOverlay.style.display = 'none';
UI._bindGalleryKeys(false);
},
showGalleryImage(index) {
const imgs = UI._galleryState.images || [];
if (!imgs.length) return;
UI._galleryState.index = (index + imgs.length) % imgs.length;
const imgDesc = imgs[UI._galleryState.index];
const url = UI.buildPreviewUrl(imgDesc, UI._galleryState.pid);
UI._galleryImg.src = url.href;
UI._galleryCounter.textContent = `${UI._galleryState.index + 1} / ${imgs.length}`;
},
nextImage() { UI.showGalleryImage(UI._galleryState.index + 1); },
prevImage() { UI.showGalleryImage(UI._galleryState.index - 1); },
_bindGalleryKeys(enabled) {
const handler = (e) => {
if (UI._galleryOverlay?.style.display !== 'flex') return;
if (e.key === 'Escape') UI.closeGallery();
else if (e.key === 'ArrowRight') UI.nextImage();
else if (e.key === 'ArrowLeft') UI.prevImage();
};
if (enabled) {
UI._galleryKeyHandler = handler;
document.addEventListener('keydown', handler);
} else if (UI._galleryKeyHandler) {
document.removeEventListener('keydown', UI._galleryKeyHandler);
UI._galleryKeyHandler = null;
}
},
ensureGalleryElements() {
if (UI._galleryOverlay) return;
UI._galleryState = { images: [], index: 0 };
const overlay = UI.el('div', { class: 'pqueue-gallery-overlay' });
const box = UI.el('div', { class: 'pqueue-gallery' });
const closeBtn = UI.el('button', { class: 'pqueue-gallery-close', title: 'Close' });
closeBtn.textContent = '×';
const img = UI.el('img', { class: 'pqueue-gallery-img' });
const counter = UI.el('div', { class: 'pqueue-gallery-counter' });
const prev = UI.el('button', { class: 'pqueue-gallery-nav pqueue-gallery-prev', title: 'Previous' });
prev.textContent = '‹';
const next = UI.el('button', { class: 'pqueue-gallery-nav pqueue-gallery-next', title: 'Next' });
next.textContent = '›';
closeBtn.onclick = () => UI.closeGallery();
prev.onclick = () => UI.prevImage();
next.onclick = () => UI.nextImage();
overlay.onclick = (e) => { if (e.target === overlay) UI.closeGallery(); };
box.appendChild(closeBtn);
box.appendChild(img);
box.appendChild(counter);
box.appendChild(prev);
box.appendChild(next);
overlay.appendChild(box);
document.body.appendChild(overlay);
UI._galleryOverlay = overlay;
UI._galleryImg = img;
UI._galleryCounter = counter;
},
ensureStyles() {
if (document.getElementById('pqueue-styles')) return;
const style = document.createElement('style');
style.id = 'pqueue-styles';
style.textContent = `
#pqueue-fab{position:fixed;right:16px;bottom:16px;z-index:99999;background:#2d2f34;color:#fff;border:none;border-radius:20px;padding:8px 12px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.35)}
#pqueue-panel{position:fixed;right:16px;top:64px;z-index:99998;width:420px;max-width:90vw;height:65vh;background:#1c1d20;color:#ddd;border:1px solid #333;border-radius:8px;box-shadow:0 6px 24px rgba(0,0,0,0.5);display:none;flex-direction:column}
#pqueue-panel header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #333}
#pqueue-panel .pqueue-content{padding:8px 12px;overflow:auto;height:100%}
#pqueue-panel ul{list-style:none;margin:0;padding:0}
#pqueue-panel li{display:flex;gap:8px;align-items:center;padding:6px 4px;border-bottom:1px dashed #2c2c2c}
#pqueue-panel .pqueue-item{cursor:grab}
#pqueue-panel .pqueue-section{margin-bottom:10px}
`;
document.head.appendChild(style);
},
mount() {
if (state.container) return;
UI.ensureStyles();
// Floating button
const fab = document.createElement('button');
fab.id = 'pqueue-fab';
fab.textContent = 'Persistent Queue';
fab.onclick = () => {
const panel = document.getElementById('pqueue-panel');
if (!panel) return;
const visible = panel.style.display !== 'none';
panel.style.display = visible ? 'none' : 'flex';
if (!visible) refresh();
};
document.body.appendChild(fab);
const panel = UI.el('div', { id: 'pqueue-panel', class: 'pqueue-panel' });
const header = document.createElement('header');
const title = document.createElement('div');
title.textContent = 'Persistent Queue';
const close = document.createElement('button');
close.textContent = '×';
close.onclick = () => { panel.style.display = 'none'; };
header.appendChild(title);
header.appendChild(close);
panel.appendChild(header);
state.container = UI.el('div', { class: 'pqueue-content' });
panel.appendChild(state.container);
document.body.appendChild(panel);
}
};
const Events = {
async onPauseClick() {
if (state.paused) await API.resume(); else await API.pause();
await refresh();
},
async onClearClick() {
const ids = Array.from(document.querySelectorAll('#pqueue-pending li[data-id]')).map(li => li.dataset.id);
if (ids.length) {
await API.del(ids);
await refresh();
}
},
onPendingListItemCheckboxChange(e) {
const li = e.target.closest('li');
const id = li.dataset.id;
if (e.target.checked) state.selectedPending.add(id); else state.selectedPending.delete(id);
},
async onPendingListItemPrioritySaveClick(e) {
const li = e.target.closest('li');
const id = li.dataset.id;
const val = parseInt(li.querySelector('.pqueue-priority').value || '0', 10);
await API.setPriority(id, val);
await refresh();
},
async onPendingListItemDeleteClick(e) {
const li = e.target.closest('li');
const id = li.dataset.id;
await API.del([id]);
await refresh();
},
init() {
document.getElementById('pqueue-toggle').onclick = Events.onPauseClick;
const clearBtn = document.getElementById('pqueue-clear');
if (clearBtn) {
clearBtn.onclick = Events.onClearClick;
}
const list = document.getElementById('pqueue-pending');
if (!list) return;
let dragged;
list.addEventListener('dragstart', (e) => {
const li = e.target.closest('li');
dragged = li;
e.dataTransfer.setData('text/plain', li.dataset.id);
});
list.addEventListener('dragover', (e) => e.preventDefault());
list.addEventListener('drop', async (e) => {
e.preventDefault();
const target = e.target.closest('li');
if (!dragged || !target || dragged === target) return;
const rect = target.getBoundingClientRect();
const before = (e.clientY - rect.top) < rect.height / 2;
if (before) list.insertBefore(dragged, target); else list.insertBefore(dragged, target.nextSibling);
const order = Array.from(list.querySelectorAll('li')).map(li => li.dataset.id);
await API.reorder(order);
await refresh();
});
list.addEventListener('change', (e) => {
if (e.target.classList.contains('pqueue-select')) {
Events.onPendingListItemCheckboxChange(e);
}
});
list.addEventListener('click', (e) => {
if (e.target.closest('.pqueue-priority-save')) {
Events.onPendingListItemPrioritySaveClick(e);
} else if (e.target.closest('.pqueue-delete')) {
Events.onPendingListItemDeleteClick(e);
}
});
}
};
async function refresh() {
const q = await API.getQueue();
state.paused = !!q.paused;
state.queue_running = q.queue_running || [];
state.queue_pending = q.queue_pending || [];
state.db_pending = q.db_pending || [];
state.running_progress = q.running_progress || {};
const h = await API.getHistory(50);
state.history = h.history || [];
UI.render();
}
function setupSocketListeners() {
try {
const api = window.app?.api;
if (!api || typeof api.addEventListener !== 'function') {
console.warn('pqueue: WebSocket API not found, falling back to polling.');
return false;
}
const onProgressState = (ev) => {
try {
const payload = ev?.detail;
if (!payload?.prompt_id || !payload.nodes) return;
state.running_progress[payload.prompt_id] = Progress.computeAggregateProgress(payload.nodes);
UI.updateProgressBarsFromState();
} catch (e) { /* ignore */ }
};
// Use immediate refresh on lifecycle events; progress updates are handled separately
const onQueueOrExecChange = () => refresh();
const onExecDone = () => refresh();
api.addEventListener('progress_state', onProgressState);
api.addEventListener('status', onQueueOrExecChange);
// 'executing' fires often per-node; do not refresh on every tick
api.addEventListener('executing', onProgressState);
api.addEventListener('executed', onExecDone);
api.addEventListener('execution_start', onQueueOrExecChange);
api.addEventListener('execution_success', onExecDone);
api.addEventListener('execution_error', onExecDone);
api.addEventListener('execution_interrupted', onExecDone);
// If we reach here, socket listeners are active. Ensure fallback polling is disabled.
stopFallbackPolling();
return true;
} catch (e) {
console.error('pqueue: Error setting up WebSocket listeners.', e);
return false;
}
}
function stopFallbackPolling() {
try {
if (_pqueuePollIntervalId) {
clearInterval(_pqueuePollIntervalId);
_pqueuePollIntervalId = null;
}
if (_pqueueFocusListener) {
window.removeEventListener('focus', _pqueueFocusListener);
_pqueueFocusListener = null;
}
} catch (e) { /* noop */ }
}
function setupFallbackPolling() {
// Only used when WebSocket API is unavailable
stopFallbackPolling();
_pqueueFocusListener = () => refresh();
window.addEventListener('focus', _pqueueFocusListener);
_pqueuePollIntervalId = setInterval(refresh, 3000);
}
function initialize() {
const finalSetup = (el) => {
state.container = el;
UI.render();
if (!setupSocketListeners()) {
setupFallbackPolling();
// Retry establishing WebSocket listeners in the background
let retries = 50;
const retryId = setInterval(() => {
if (setupSocketListeners() || --retries <= 0) {
clearInterval(retryId);
}
}, 200);
}
// Initial fetch to populate queue + history on first load
refresh();
};
const sidebarTabDefinition = {
id: 'persistent_queue',
icon: 'pi pi-history',
title: 'Persistent Queue',
tooltip: 'Persistent Queue',
type: 'custom',
render: (el) => {
document.getElementById('pqueue-fab')?.remove();
document.getElementById('pqueue-panel')?.remove();
finalSetup(el);
},
};
const tryRegisterSidebar = () => {
try {
if (window.app?.extensionManager?.registerSidebarTab) {
window.app.extensionManager.registerSidebarTab(sidebarTabDefinition);
return true;
}
} catch (error) {
console.error("pqueue: Error registering sidebar tab.", error);
}
return false;
};
const initLogic = () => {
if (tryRegisterSidebar()) {
return;
}
if (!document.getElementById('pqueue-panel')) {
UI.mount();
finalSetup(state.container);
}
let retries = 50; // Try for 10 seconds
const intervalId = setInterval(() => {
if (tryRegisterSidebar() || --retries <= 0) {
clearInterval(intervalId);
}
}, 200);
};
const extension = {
name: 'ComfyUI-PersistentQueue',
async setup(app) {
initLogic();
}
};
// Use the modern extension system if available.
if (window.app?.registerExtension) {
window.app.registerExtension(extension);
} else {
// Fallback for very old versions.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLogic);
} else {
initLogic();
}
}
}
initialize();
})();