From a17314673e413c6eaedfb9416a33a5d27e31fefc Mon Sep 17 00:00:00 2001 From: m0rtus59 Date: Fri, 11 Sep 2026 00:59:32 +0500 Subject: [PATCH] Job preview fixes and improvements --- js/queue/executionTracker.js | 33 ++++++++++++++++++++--- js/queue/queueService.js | 20 +++++++++++++- js/sidebar.js | 6 +++-- js/ui/cardRenderer.js | 52 +++++++++++++++++++++++------------- js/ui/mediaPreview.js | 18 ++++++++++++- js/utils/comparison.js | 11 +++++++- js/utils/utils.js | 7 +++++ pyproject.toml | 2 +- 8 files changed, 121 insertions(+), 28 deletions(-) diff --git a/js/queue/executionTracker.js b/js/queue/executionTracker.js index 3becd14..029862b 100644 --- a/js/queue/executionTracker.js +++ b/js/queue/executionTracker.js @@ -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) diff --git a/js/queue/queueService.js b/js/queue/queueService.js index 6e3465a..71730f6 100644 --- a/js/queue/queueService.js +++ b/js/queue/queueService.js @@ -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); diff --git a/js/sidebar.js b/js/sidebar.js index 0e331e3..ee2dde4 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -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(); } diff --git a/js/ui/cardRenderer.js b/js/ui/cardRenderer.js index 83250a1..4727715 100644 --- a/js/ui/cardRenderer.js +++ b/js/ui/cardRenderer.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,7 +153,9 @@ 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) { cardObj.leftHoverBtn.style.removeProperty("display"); @@ -174,7 +176,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 +186,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 = {}) { diff --git a/js/ui/mediaPreview.js b/js/ui/mediaPreview.js index aaf9a7b..ca1bb71 100644 --- a/js/ui/mediaPreview.js +++ b/js/ui/mediaPreview.js @@ -427,7 +427,18 @@ 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 () => { @@ -446,6 +457,11 @@ export function renderCardImages(cardObj, state, onNavigateBatch) { 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"; } diff --git a/js/utils/comparison.js b/js/utils/comparison.js index b751c1b..22bd12e 100644 --- a/js/utils/comparison.js +++ b/js/utils/comparison.js @@ -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; + } } \ No newline at end of file diff --git a/js/utils/utils.js b/js/utils/utils.js index 12fa2df..4ca537c 100644 --- a/js/utils/utils.js +++ b/js/utils/utils.js @@ -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 || []; } diff --git a/pyproject.toml b/pyproject.toml index 5c50f8d..8fdf18d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.1" license = { file = "LICENSE" } dependencies = []