Compare commits

..
3 Commits
10 changed files with 375 additions and 79 deletions
+29 -4
View File
@@ -5,6 +5,7 @@ import { PromptStatus } from "../core/constants.js";
import { createPromptState, activatePrompt, updatePromptProgress } from "../core/promptState.js";
import { syncQueue, concludeRun, initSessionAndHistory, clearCancelledOrFailed } from "./queueService.js";
import { findImagesInOutputs, findTextsInOutputs } from "../utils/utils.js";
import { updateActiveComparisonPreview } from "../utils/comparison.js";
export function setupExecutionTracker(onTargetedProgressUpdate) {
const onStatus = () => syncQueue();
@@ -68,9 +69,10 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
const showWorkingNode = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Show Working Node Name") ?? true;
const activePid = store.ui.currentlyActivePromptId;
if (showWorkingNode && activePid && store.hasPrompt(activePid)) {
if (activePid && store.hasPrompt(activePid)) {
const prompt = store.getPrompt(activePid);
if (nodeId) {
prompt.activeNodeId = String(nodeId);
const node = app.graph?.getNodeById ? app.graph.getNodeById(nodeId) : null;
prompt.activeNodeName = node ? (node.title || node.type) : `Node #${nodeId}`;
} else {
@@ -78,7 +80,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
}
store.updatePrompt(activePid, prompt);
if (typeof onTargetedProgressUpdate === "function") {
if (showWorkingNode && typeof onTargetedProgressUpdate === "function") {
onTargetedProgressUpdate(activePid, prompt.progress, prompt.activeNodeName);
}
}
@@ -94,6 +96,14 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
prompt._previewBlobUrl = newBlobUrl;
prompt.images = [{ url: newBlobUrl }];
store.updatePrompt(prompt.pid, prompt);
// Update live fullscreen preview if currently open
updateActiveComparisonPreview(prompt.pid, newBlobUrl);
// Update the active card in the sidebar
if (typeof onTargetedProgressUpdate === "function") {
onTargetedProgressUpdate(prompt.pid, prompt.progress, prompt.activeNodeName);
}
}
};
@@ -102,8 +112,14 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
if (pid && store.hasPrompt(pid)) {
const prompt = store.getPrompt(pid);
const nodeImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
if (nodeImgs.length > 0) {
// Only update prompt.images if this node actually produced real output images
const hasRealNewImgs = nodeImgs.some(img => !img.isFallback);
if (hasRealNewImgs) {
prompt.images = nodeImgs;
// If fullscreen is open, smoothly transition to the final saved image
const finalImg = nodeImgs[0];
const finalSrc = finalImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(finalImg.filename)}&type=${finalImg.type || 'output'}&subfolder=${encodeURIComponent(finalImg.subfolder || '')}`);
updateActiveComparisonPreview(pid, finalSrc);
}
const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow);
if (nodeTexts.length > 0) {
@@ -118,7 +134,16 @@ export function setupExecutionTracker(onTargetedProgressUpdate) {
};
const onExecutionSuccess = (e) => concludeRun(e.detail.prompt_id, PromptStatus.COMPLETED);
const onExecutionError = (e) => concludeRun(e.detail.prompt_id, PromptStatus.ERROR);
const onExecutionError = (e) => {
const pid = e.detail?.prompt_id;
const nodeId = e.detail?.node_id;
if (pid && nodeId && store.hasPrompt(String(pid))) {
const prompt = store.getPrompt(String(pid));
prompt.activeNodeId = String(nodeId);
store.updatePrompt(String(pid), prompt);
}
concludeRun(e.detail.prompt_id, PromptStatus.ERROR);
};
const onExecutionInterrupted = () => {
store.getAllPrompts()
.filter(t => t.status === PromptStatus.ACTIVE)
+19 -1
View File
@@ -128,6 +128,10 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
const state = store.getPrompt(key);
finalizePrompt(state, fallbackStatus);
// If cancelled while actively sampling, preserve the exact step preview where it was cancelled
const isCancelled = fallbackStatus === PromptStatus.CANCELLED || state.status === PromptStatus.CANCELLED;
const hasActivePreviewBlob = Boolean(state._previewBlobUrl || state.images?.some(img => img.url?.startsWith("blob:")));
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(`/history/${key}`);
@@ -144,7 +148,14 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
Object.assign(state.nodeTitles, titles);
const primaryImgs = getPrimaryOutputImages(norm.outputs, state.workflow);
if (primaryImgs.length > 0) state.images = primaryImgs;
const hasActualSavedOutputs = primaryImgs.some(img => !img.isFallback);
// If cancelled while sampling, do NOT overwrite with older nodes; keep the exact step preview!
if (!isCancelled || !hasActivePreviewBlob) {
if (hasActualSavedOutputs || (state.status === PromptStatus.COMPLETED && primaryImgs.length > 0)) {
state.images = primaryImgs;
}
}
const fetchedTexts = findTextsInOutputs(norm.outputs, state.workflow);
if (fetchedTexts.length > 0) state.texts = fetchedTexts;
@@ -154,6 +165,13 @@ export async function concludeRun(pid, fallbackStatus = PromptStatus.CANCELLED)
await new Promise(r => setTimeout(r, 200));
}
// Ensure cancelled prompt keeps its preview blob if it had one
if (isCancelled && hasActivePreviewBlob) {
if (state._previewBlobUrl && (!state.images || state.images.length === 0 || !state.images[0].url?.startsWith("blob:"))) {
state.images = [{ url: state._previewBlobUrl }];
}
}
const maxItems = app.ui?.settings?.getSettingValue?.("Comfy.Queue.MaxHistoryItems") ?? 64;
store.pruneHistory(maxItems);
store.updatePrompt(key, state);
+4 -2
View File
@@ -9,6 +9,7 @@ import { initSessionAndHistory, syncQueue } from "./queue/queueService.js";
import { setupExecutionTracker } from "./queue/executionTracker.js";
import { setupSidebarView, renderSidebar, teardownSidebarView } from "./ui/sidebarView.js";
import { updateCardProgressTargeted, cardPool } from "./ui/cardRenderer.js";
import { store } from "./core/store.js";
let isInitialized = false;
let activeKeydownHandler = null;
@@ -178,9 +179,10 @@ app.registerExtension({
// Setup execution tracker with targeted fast-path update for active card
cleanupFns.push(setupExecutionTracker((pid, progress, nodeName) => {
const cardObj = cardPool.get(pid);
if (cardObj) {
const state = store.getPrompt(pid);
if (cardObj && state) {
const showWorkingNode = app.ui.settings.getSettingValue(SettingIds.SHOW_WORKING_NODE_NAME) ?? true;
updateCardProgressTargeted(cardObj, progress, nodeName, showWorkingNode);
updateCardProgressTargeted(cardObj, progress, nodeName, showWorkingNode, state);
} else {
renderSidebar();
}
+95 -21
View File
@@ -2,7 +2,7 @@ import { app } from "/scripts/app.js";
import { PromptStatus } from "../core/constants.js";
import { store } from "../core/store.js";
import { renderCardImages } from "./mediaPreview.js";
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage } from "./mediaActions.js";
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
import { centerAndSelectCanvasNode } from "../comfy/adapter.js";
import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/utils.js";
import { showFullscreenPreview } from "../utils/comparison.js";
@@ -31,9 +31,9 @@ export function syncCardButtonVisibility(cardObj, state) {
if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.removeProperty("display");
}
const isCompleted = state.status === PromptStatus.COMPLETED;
const hasRealDiskFiles = isCompleted && state.images && state.images.length > 0 &&
!state.images.every(img => img.isFallback || (img.url && img.url.startsWith("blob:")));
// Check if the card has an actual verified file saved on disk
const hasRealDiskFiles = state.images && state.images.length > 0 &&
state.images.some(img => img.filename && !img.isFallback && (!img.url || !img.url.startsWith("blob:")));
const hasRealImages = hasRealDiskFiles && !state.images.some(img => img.url && img.url.startsWith("blob:"));
const currentImg = hasRealImages ? state.images[cardObj.currentImageIndex || 0] : null;
@@ -110,26 +110,26 @@ export function syncCardButtonVisibility(cardObj, state) {
}
if (cardObj.btnDel) {
// Only show delete button if we have actual verified output files on disk to delete
if (hasRealDiskFiles) {
cardObj.btnDel.style.removeProperty("display");
cardObj.btnDel.style.display = "inline-flex";
cardObj.canDeleteFromDisk = true;
if (cardObj.btnDelLabel) {
cardObj.btnDelLabel.style.display = "";
}
cardObj.btnDel.title = "Delete Card (Hold Ctrl to delete file from disk)";
} else {
cardObj.btnDel.style.setProperty("display", "none", "important");
cardObj.canDeleteFromDisk = false;
// Always show the delete button so cards can be cleared from history
cardObj.btnDel.style.removeProperty("display");
cardObj.btnDel.style.display = "inline-flex";
cardObj.canDeleteFromDisk = hasRealDiskFiles;
if (cardObj.btnDelLabel) {
cardObj.btnDelLabel.style.display = hasRealDiskFiles ? "" : "none";
}
cardObj.btnDel.title = hasRealDiskFiles
? "Delete Card (Hold Ctrl to delete file from disk)"
: "Delete Card";
}
if (cardObj.btnFocus) {
const nodeId = currentImg ? findNodeIdForImage(state, currentImg) : null;
// Focus the image's node, or the node where the workflow stopped/errored
const nodeId = (currentImg ? findNodeIdForImage(state, currentImg) : null) || state.activeNodeId;
if (nodeId) {
cardObj.btnFocus.style.removeProperty("display");
cardObj.btnFocus.style.display = "inline-flex";
cardObj.btnFocus.title = (!currentImg && state.activeNodeId) ? "Show Stopped/Failed Node" : "Show Node";
cardObj.btnFocus.onclick = (ev) => {
ev.stopPropagation();
centerAndSelectCanvasNode(nodeId);
@@ -153,20 +153,80 @@ export function syncCardButtonVisibility(cardObj, state) {
}
}
const hasIntermediates = validOutputs.length > 1 && distinctOutputs.length > 0;
// Show intermediate button if run completed with multiple outputs, or if stopped with saved outputs
const hasIntermediates = (validOutputs.length > 1 && distinctOutputs.length > 0) ||
(state.status !== PromptStatus.COMPLETED && validOutputs.length > 0);
if (hasIntermediates) {
// Check if intermediate files still exist on disk
for (const out of distinctOutputs) {
for (const img of out.images) {
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
if (!src.startsWith("blob:")) {
fetch(src, { method: "HEAD" }).then(res => {
if (res.status === 404) {
const liveState = store.getPrompt(state.pid);
if (liveState && liveState.nodeOutputs) {
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
store.updatePrompt(liveState.pid, liveState);
syncCardButtonVisibility(cardObj, liveState);
}
}
}).catch(() => {});
}
}
}
cardObj.leftHoverBtn.style.removeProperty("display");
cardObj.leftHoverBtn.style.display = "inline-flex";
cardObj.leftHoverBtn.onclick = (ev) => {
cardObj.leftHoverBtn.onclick = async (ev) => {
ev.stopPropagation();
// On click: verify distinct outputs still exist before opening
const liveState = store.getPrompt(state.pid) || state;
const curOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
const primSigs = new Set((liveState.images || []).map(i => `${i.subfolder || ""}/${i.filename}`));
const curDistinct = curOutputs.filter(o => o.images && o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
if (curDistinct.length === 0) {
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
return;
}
let anyMissing = false;
for (const out of curDistinct) {
for (const img of out.images) {
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
if (!src.startsWith("blob:")) {
try {
const res = await fetch(src, { method: "HEAD" });
if (res.status === 404) {
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
anyMissing = true;
}
} catch (e) {}
}
}
}
if (anyMissing) {
store.updatePrompt(liveState.pid, liveState);
syncCardButtonVisibility(cardObj, liveState);
const remaining = getRunOutputs(liveState.nodeOutputs, liveState.workflow)
.filter(o => o.images && o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
if (remaining.length === 0) {
return; // All intermediate outputs were wiped; button is now hidden
}
}
const scrollEl = document.querySelector('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|| store.ui.cardStack?.parentElement
|| store.ui.cardStack;
if (scrollEl) {
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
}
store.openOutputsSubmenu(state.pid);
store.openOutputsSubmenu(liveState.pid);
};
} else {
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
@@ -174,7 +234,7 @@ export function syncCardButtonVisibility(cardObj, state) {
}
}
export function updateCardProgressTargeted(cardObj, progress, activeNodeName, showWorkingNode = true) {
export function updateCardProgressTargeted(cardObj, progress, activeNodeName, showWorkingNode = true, state = null) {
if (!cardObj) return;
if (cardObj.progressBar) {
cardObj.progressBar.style.width = `${progress}%`;
@@ -184,6 +244,20 @@ export function updateCardProgressTargeted(cardObj, progress, activeNodeName, sh
const nodeText = activeNodeName ? (activeNodeName === "Finishing..." ? "Finishing..." : `[${activeNodeName}]`) : "Processing...";
cardObj.statusText.textContent = `${nodeText} ${progress}%`;
}
// Fast-path: update live preview image directly on active card
if (state && state.images && state.images.length > 0) {
const firstImg = state.images[0];
const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}` : null);
if (previewUrl && cardObj.firstImgElement && cardObj.firstImgElement.tagName === "IMG") {
const currentSrc = cardObj.firstImgElement.getAttribute("src") || cardObj.firstImgElement.src;
if (currentSrc !== previewUrl && !cardObj.firstImgElement.src.endsWith(previewUrl)) {
cardObj.firstImgElement.src = previewUrl;
}
} else {
updateCardDOM(cardObj, state, true, showWorkingNode);
}
}
}
export function getOrCreateCard(state, callbacks = {}) {
+101 -12
View File
@@ -1,8 +1,9 @@
import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "../utils/utils.js";
import { openFileOrFolder } from "./mediaActions.js";
import { openFileOrFolder, removeImageFromNodeOutputs } from "./mediaActions.js";
import { stopAllAudioPlayback, isAudioViewerOpen, setCurrentlyPlayingAudio, getCurrentlyPlayingAudio } from "./audioController.js";
import { showFullscreenPreview } from "../utils/comparison.js";
import { store } from "../core/store.js";
import { PromptStatus } from "../core/constants.js";
export function render3DCardPreview(cardObj, wrapper, src, img, state) {
let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
@@ -272,6 +273,9 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
}
export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state) {
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (playIcon) playIcon.remove();
let previewFile = wrapper.querySelector(".comfy-sidebar-file-wrapper");
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
const filename = img.filename || getFilenameFromUrl(src) || "output_file";
@@ -427,25 +431,110 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
openFileOrFolder(img);
return;
}
showFullscreenPreview([src], ev.shiftKey);
let activeSrc = mediaEl.currentSrc || mediaEl.src || src;
// If it's a preview blob, snapshot the rendered pixels from the card so revoked blobs don't 404 in fullscreen
if (activeSrc && activeSrc.startsWith("blob:") && mediaEl.naturalWidth > 0) {
try {
const canvas = document.createElement("canvas");
canvas.width = mediaEl.naturalWidth;
canvas.height = mediaEl.naturalHeight;
canvas.getContext("2d").drawImage(mediaEl, 0, 0);
activeSrc = canvas.toDataURL("image/png");
} catch (_) {}
}
showFullscreenPreview([activeSrc], ev.shiftKey, state?.pid);
};
mediaEl.onerror = async () => {
if (src && !src.startsWith("blob:") && !is3D && !isAudio) {
try {
const res = await fetch(src, { method: "HEAD" });
if (res.status === 404) {
store.deletePrompt(state.pid);
if (cardObj.element) cardObj.element.remove();
return;
mediaEl.onerror = () => {
mediaEl.onerror = null;
if ((src && src.startsWith("blob:")) || is3D || isAudio) return;
const cardId = cardObj.element?.id || "";
// 1. If an intermediate output item is missing, remove only this item
if (cardId.startsWith("card-submenu-")) {
const parentState = store.getPrompt(state.pid);
if (parentState && parentState.nodeOutputs) {
if (state.nodeId && parentState.nodeOutputs[state.nodeId]) {
delete parentState.nodeOutputs[state.nodeId];
} else {
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
}
} catch (e) {}
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
store.updatePrompt(parentState.pid, parentState);
}
cardObj.element?.remove();
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
if (!remaining || remaining.length === 0) {
store.closeSubmenu();
}
return;
}
// 2. If a batch item is missing, remove only this item from the batch
if (cardId.startsWith("card-batch-")) {
const parentPid = state.parentPromptId || state.pid;
const parentState = store.getPrompt(parentPid);
if (parentState) {
if (Array.isArray(parentState.images)) {
const idx = parentState.images.findIndex(i => i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""));
if (idx > -1) parentState.images.splice(idx, 1);
}
if (parentState.nodeOutputs) {
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
}
store.updatePrompt(parentPid, parentState);
}
cardObj.element?.remove();
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
if (!remaining || remaining.length === 0) {
store.closeSubmenu();
}
return;
}
// 3. Main card: if this file is gone, clean it up
const liveState = store.getPrompt(state.pid);
if (liveState) {
if (Array.isArray(liveState.images)) {
liveState.images = liveState.images.filter(i =>
!(i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""))
);
}
if (liveState.nodeOutputs) {
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
}
// If the card still has other images (e.g. from a batch), persist and switch to next image
if (liveState.images && liveState.images.length > 0) {
cardObj.currentImageIndex = 0;
cardObj.lastImagesSignature = "";
store.updatePrompt(liveState.pid, liveState);
return;
}
// If the card has text outputs or is still in progress, persist
if ((liveState.texts && liveState.texts.length > 0) || (liveState.status && liveState.status !== PromptStatus.COMPLETED)) {
cardObj.lastImagesSignature = "";
store.updatePrompt(liveState.pid, liveState);
return;
}
// Only delete the main card when all items belonging to it are gone
store.deletePrompt(liveState.pid);
cardObj.element?.remove();
return;
}
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
};
const applyDimensions = (width, height) => {
if (cardObj.dimEl && width && height) {
// Don't show the dimension badge for active/running preview frames
if (state.status === "active") {
cardObj.dimEl.style.display = "none";
return;
}
cardObj.dimEl.textContent = `${width}x${height}`;
cardObj.dimEl.style.display = "block";
}
+108 -36
View File
@@ -12,6 +12,80 @@ let scrollToTopBtnEl = null;
let globalClickRegistered = false;
let renderAnimationFrameId = null;
let activeTimerInterval = null;
let currentColumnCount = 1;
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
if (!cardStack) return;
// 1. Full-width container (for banners like Pending Queue summary)
let bannerContainer = cardStack.querySelector('.comfy-sidebar-banners');
if (!bannerContainer) {
bannerContainer = document.createElement('div');
bannerContainer.className = 'comfy-sidebar-banners';
Object.assign(bannerContainer.style, { display: 'flex', flexDirection: 'column', gap: '12px', width: '100%' });
cardStack.insertBefore(bannerContainer, cardStack.firstChild);
}
fullWidthEls.forEach((el, i) => {
if (bannerContainer.children[i] !== el) {
bannerContainer.insertBefore(el, bannerContainer.children[i] || null);
}
});
while (bannerContainer.children.length > fullWidthEls.length) {
bannerContainer.removeChild(bannerContainer.lastChild);
}
bannerContainer.style.display = fullWidthEls.length > 0 ? 'flex' : 'none';
bannerContainer.style.marginBottom = fullWidthEls.length > 0 ? '12px' : '0';
// 2. Masonry Columns container
let columnsWrapper = cardStack.querySelector('.comfy-sidebar-masonry-wrapper');
if (!columnsWrapper) {
columnsWrapper = document.createElement('div');
columnsWrapper.className = 'comfy-sidebar-masonry-wrapper';
Object.assign(columnsWrapper.style, { display: 'flex', gap: '12px', alignItems: 'flex-start', width: '100%' });
cardStack.appendChild(columnsWrapper);
}
while (columnsWrapper.children.length < cols) {
const colDiv = document.createElement('div');
colDiv.className = 'comfy-sidebar-masonry-col';
Object.assign(colDiv.style, { flex: '1', display: 'flex', flexDirection: 'column', gap: '12px', minWidth: '0' });
columnsWrapper.appendChild(colDiv);
}
while (columnsWrapper.children.length > cols) {
columnsWrapper.removeChild(columnsWrapper.lastChild);
}
// Distribute cards into the shortest column so short cards stack next to tall cards
const colBuckets = Array.from({ length: cols }, () => []);
const colHeights = new Array(cols).fill(0);
cardEls.forEach((cardEl) => {
let shortestCol = 0;
for (let c = 1; c < cols; c++) {
if (colHeights[c] < colHeights[shortestCol]) {
shortestCol = c;
}
}
colBuckets[shortestCol].push(cardEl);
const cardH = cardEl.offsetHeight > 0 ? cardEl.offsetHeight : (cardEl._cachedHeight || 160);
cardEl._cachedHeight = cardH;
colHeights[shortestCol] += cardH + 12;
});
for (let c = 0; c < cols; c++) {
const colDiv = columnsWrapper.children[c];
const bucket = colBuckets[c];
bucket.forEach((el, i) => {
if (colDiv.children[i] !== el) {
colDiv.insertBefore(el, colDiv.children[i] || null);
}
});
while (colDiv.children.length > bucket.length) {
colDiv.removeChild(colDiv.lastChild);
}
}
}
function getScrollContainer() {
if (!store.ui.cardStack) return null;
@@ -213,7 +287,13 @@ export function setupSidebarView() {
const cardStack = document.createElement("div");
store.ui.cardStack = cardStack;
Object.assign(cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
Object.assign(cardStack.style, {
flex: "1",
overflowY: "visible",
display: "flex",
flexDirection: "column",
paddingBottom: "28px"
});
container.appendChild(cardStack);
scrollToTopBtnEl = document.createElement("button");
@@ -233,9 +313,9 @@ export function setupSidebarView() {
new ResizeObserver((entries) => {
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
const cols = Math.max(1, Math.floor(entries[0].contentRect.width / (threshold / 2)));
if (store.ui.cardStack) {
store.ui.cardStack.style.columnCount = cols.toString();
store.ui.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
if (cols !== currentColumnCount) {
currentColumnCount = cols;
renderSidebar();
}
}).observe(container);
@@ -504,14 +584,7 @@ export function renderSidebar() {
targetElements.push(cardObj.element);
});
targetElements.forEach((el, index) => {
if (store.ui.cardStack.children[index] !== el) {
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
}
});
while (store.ui.cardStack.children.length > targetElements.length) {
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
}
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
const scrollEl = getScrollContainer();
if (scrollEl) scrollEl.scrollTop = 0;
@@ -568,7 +641,8 @@ export function renderSidebar() {
const emptyCard = document.createElement("div");
emptyCard.className = "comfy-sidebar-card completed";
Object.assign(emptyCard.style, {
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px"
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px",
gridColumn: "1 / -1"
});
emptyCard.textContent = "No separate intermediate outputs found.";
targetElements.push(emptyCard);
@@ -749,6 +823,7 @@ export function renderSidebar() {
cardObj.placeholder.style.display = "none";
renderCardImages(cardObj, {
pid: st.pid,
nodeId: out.nodeId,
status: PromptStatus.COMPLETED,
images: out.images,
workflow: st.workflow,
@@ -777,13 +852,10 @@ export function renderSidebar() {
});
}
targetElements.forEach((el, index) => {
if (store.ui.cardStack.children[index] !== el) {
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
}
});
while (store.ui.cardStack.children.length > targetElements.length) {
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
if (outputs.length === 0) {
mountCards(store.ui.cardStack, targetElements, [], currentColumnCount);
} else {
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
}
updateScrollTopBtnVisibility();
@@ -817,7 +889,8 @@ export function renderSidebar() {
return updateCardDOM(cardObj, state, showPendingSummary, showWorkingNode);
};
const targetElements = [];
const bannerElements = [];
const cardElements = [];
const pendingCount = store.getAllPrompts().filter(t => t.status === PromptStatus.PENDING).length;
if (pendingCount > 0) {
@@ -827,8 +900,8 @@ export function renderSidebar() {
const el = document.createElement("div");
Object.assign(el.style, {
background: "var(--comfy-input-bg, #181818)", border: "2px solid #6c757d", borderRadius: "4px", padding: "10px",
marginBottom: "12px", textAlign: "center", fontSize: "12px", fontWeight: "bold",
color: "var(--desc-color, #aaa)", breakInside: "avoid", display: "flex", flexDirection: "column", gap: "8px"
textAlign: "center", fontSize: "12px", fontWeight: "bold",
color: "var(--desc-color, #aaa)", display: "flex", flexDirection: "column", gap: "8px"
});
const textDiv = document.createElement("div");
el.appendChild(textDiv);
@@ -846,7 +919,7 @@ export function renderSidebar() {
cardPool.set("pending-summary-card", pCard);
}
pCard.textDiv.textContent = `Pending Queue: ${pendingCount} tasks`;
targetElements.push(pCard.element);
bannerElements.push(pCard.element);
} else {
let btnCard = cardPool.get("pending-cancel-all-standalone");
if (!btnCard) {
@@ -854,7 +927,7 @@ export function renderSidebar() {
btn.textContent = "Cancel All Pending";
Object.assign(btn.style, {
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "6px",
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%", marginBottom: "12px", breakInside: "avoid"
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%"
});
btn.onclick = async () => {
await cancelAllPending();
@@ -862,23 +935,22 @@ export function renderSidebar() {
btnCard = { element: btn };
cardPool.set("pending-cancel-all-standalone", btnCard);
}
targetElements.push(btnCard.element);
pendingTasks.forEach(st => targetElements.push(syncTaskCard(st)));
bannerElements.push(btnCard.element);
pendingTasks.forEach(st => cardElements.push(syncTaskCard(st)));
}
}
activeTasks.forEach(st => targetElements.push(syncTaskCard(st)));
completedTasks.forEach(st => targetElements.push(syncTaskCard(st)));
activeTasks.forEach(st => cardElements.push(syncTaskCard(st)));
completedTasks.forEach(st => cardElements.push(syncTaskCard(st)));
targetElements.forEach((el, index) => {
if (store.ui.cardStack.children[index] !== el) {
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
}
});
while (store.ui.cardStack.children.length > targetElements.length) {
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
const containerWidth = store.ui.sidebarContainer?.clientWidth || 0;
if (containerWidth > 0) {
currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2)));
}
mountCards(store.ui.cardStack, bannerElements, cardElements, currentColumnCount);
if (store.ui.mainQueueScrollTop !== null) {
const restorePos = store.ui.mainQueueScrollTop;
store.ui.mainQueueScrollTop = null;
+10 -1
View File
@@ -10,6 +10,12 @@ export function isAudioViewerOpen() {
return !!(activeComparisonViewer && activeComparisonViewer.isAudio);
}
export function updateActiveComparisonPreview(pid, newSrc) {
if (activeComparisonViewer && activeComparisonViewer.targetPid === String(pid)) {
activeComparisonViewer.loadTarget(newSrc);
}
}
const getClientX = (e) => {
if (e.touches && e.touches.length > 0) return e.touches[0].clientX;
if (e.changedTouches && e.changedTouches.length > 0) return e.changedTouches[0].clientX;
@@ -675,7 +681,7 @@ function createComparisonViewer(baseSrc, onDestroy = () => {}) {
};
}
export function showFullscreenPreview(imgSrcs, isShiftClick = false) {
export function showFullscreenPreview(imgSrcs, isShiftClick = false, pid = null) {
if (!imgSrcs || imgSrcs.length === 0) return;
const item = imgSrcs[0];
@@ -751,4 +757,7 @@ export function showFullscreenPreview(imgSrcs, isShiftClick = false) {
src,
() => { activeComparisonViewer = null; }
);
if (activeComparisonViewer) {
activeComparisonViewer.targetPid = pid ? String(pid) : null;
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ export function injectStyles() {
.comfy-sidebar-card {
background: var(--comfy-input-bg, #181818);
border-radius: 4px; padding: 8px; position: relative;
min-height: 80px; margin-bottom: 12px; break-inside: avoid;
min-height: 80px; margin-bottom: 0px; break-inside: avoid;
user-select: none; -webkit-user-select: none;
border: 2px solid var(--border-color, #333);
color: var(--comfy-input-color, var(--fg-color, #eee));
+7
View File
@@ -339,6 +339,13 @@ export function getPrimaryOutputImages(nodeOutputs, rawWorkflow) {
if (!nodeOutputs) return findImagesInOutputs(null, rawWorkflow);
const runOutputs = getRunOutputs(nodeOutputs, rawWorkflow);
if (runOutputs.length === 0) return findImagesInOutputs(nodeOutputs, rawWorkflow);
// Find the latest executed node that actually produced real output images
for (let i = runOutputs.length - 1; i >= 0; i--) {
const imgs = runOutputs[i].images || [];
if (imgs.some(img => !img.isFallback)) {
return imgs;
}
}
return runOutputs[runOutputs.length - 1].images || [];
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfy-sidebar"
description = "A custom queue sidebar and image comparison extension that reintroduces and improves a sidebar queue menu and unified compact topbar layout for ComfyUI."
version = "1.5.0"
version = "1.5.3"
license = { file = "LICENSE" }
dependencies = []