Compare commits

...
20 Commits
Author SHA1 Message Date
m0rtus59 b3e6b1bd62 version bump 2026-08-08 08:08:32 +05:00
m0rtus59 2dc92d3e6f perf(queue): optimize rendering overhead and fix preview image flicker 2026-08-08 07:58:45 +05:00
m0rtus59 e83c999da7 bug fixes and improvements 2026-07-28 22:38:52 +05:00
m0rtus59 f61d6c8902 Bump version number 2026-07-27 17:56:12 +05:00
m0rtus59 689e020230 Optimize card action button styles 2026-07-26 23:34:19 +05:00
m0rtus59 ad4e138eac Fixed 'toggle properties panel' button in Comfy Layout with left-sided sidebar 2026-07-26 23:13:06 +05:00
m0rtus59 8487150e6f bump version number 2026-07-25 00:44:42 +05:00
m0rtus59 0e120f1af1 bug fixes and optimizations 2026-07-25 00:42:01 +05:00
m0rtus59 b6bdaefacc 'Replace stock job history bar' toggle now overrides the floating job history bar. Tidy up legacy code 2026-07-24 23:27:39 +05:00
m0rtus59 5adf5ea513 Add Comfy Registry integration and setup automatic publisher 2026-07-20 20:53:09 +05:00
m0rtus59 f6e76b1789 Fixed layout for user avatar and new extension manager 2026-07-20 20:20:34 +05:00
m0rtus59 0e648a9fa9 intermediate step cards display dimensions 2026-07-19 16:36:04 +05:00
M0rtus59 684726789f Show intermediate results on cancelled runs if any. Improved drag&drop logic 2026-07-19 14:22:59 +05:00
M0rtus59 c2c82065a7 Drag&drop for unfinished tasks hooks up the workflow JSON. Updated readme 2026-07-19 12:26:17 +05:00
M0rtus59 5fde92b51a Added 'Ignore in queue' toggle 2026-07-19 11:52:29 +05:00
m0rtus59 c43863a219 Modify preview compare slider instructions in README
Updated the instructions for using the preview compare slider to include the shift+click action.
2026-07-19 09:56:48 +05:00
M0rtus59 111de1a8c2 Improved comparison 2026-07-19 09:55:12 +05:00
M0rtus59 92653bfa75 minor fixes 2026-07-19 00:56:46 +05:00
M0rtus59 bf2cdc4bcd Add show node button 2026-07-19 00:39:00 +05:00
M0rtus59 d85c54232a Fixed missing batch arrows 2026-07-19 00:21:04 +05:00
12 changed files with 1403 additions and 639 deletions
+42
View File
@@ -0,0 +1,42 @@
name: Publish to Comfy registry & GitHub Release
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
permissions:
contents: write # Gives GitHub permission to create tags & releases
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
# 1. Read version string from pyproject.toml
- name: Extract Version
id: get_version
run: |
VERSION=$(grep -m 1 'version' pyproject.toml | cut -d '"' -f 2)
echo "VERSION=$VERSION" >> $GITHUB_ENV
# 2. Automatically create Git Tag & GitHub Release
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ env.VERSION }}
name: v${{ env.VERSION }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -3
View File
@@ -13,7 +13,8 @@ A lightweight, high-performance ComfyUI extension that replaces the cluttered st
* **Live Queue Tracking**: Stay informed on your server state. Includes a visual progress bar that tracks generations dynamically and displays the exact node title currently being executed.
* A compact topbar layout displaying both workflow tabs and the control panel on a single unified top bar. See the 'Comfy Layout' toggle in the settings.
* Toggles to hide buttons you don't need from the side panel.
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then click another to load it alongside the first one for pixel by pixel comparison.
* **[NEW]** Preview compare slider - just click the result image preview in the sidebar, and then `Shift+Click` another to load it alongside the first one for pixel by pixel comparison.
* **[NEW]** Prevent selected nodes from showing the output in the queue bar and intermediate results. `Ctrl+Q` to toggle.
<img width="385" height="336" alt="image" src="https://github.com/user-attachments/assets/743fbf85-9281-4561-9dd0-cd9af4d8c0e4" />
@@ -32,8 +33,10 @@ Restart your ComfyUI server and refresh the browser tab.
## ⚙️ Configuration
Additional tweaks are seamlessly integrated into the standard ComfyUI Settings panel. From there, you can adjust thresholds or fully replace the stock V2 panel by enabling options like **"Override Stock Job History Tab"**.
See additional tweaks in ComfyUI settings menu > Comfy Sidebar. From there, you can adjust the layout or fully replace the stock V2 panel by enabling options like **"Comfy Layout"** and **"Replace the stock Job History sidebar with Comfy Queue"**.
## ⌨️ Shortcuts
* **`Q`**: Instantly toggles the sidebar panel open or closed. Safely ignores inputs if you are currently typing inside a text field, input area, or combo box.
* **`Q`**: Toggle sidebar queue panel.
* **`Ctrl+Q on a selected node`**: Sets the node to be ignored in the sidebar panel results.
* **`Click`** on a card to enter preview. **`Shift+Click`** on another card in preview to enter compare mode.
+94 -52
View File
@@ -1,9 +1,14 @@
import { State } from "./state.js";
import { isVideoFormat } from "./utils.js";
let activeComparisonViewer = null;
let videoSyncActive = false;
let syncAnimationFrameId = null;
let globalKeydownHandler = null;
// Safe clientX resolver for mouse and touch events
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;
return e.clientX || 0;
};
const createMediaElement = (src, muted = false) => {
const isVideo = isVideoFormat(src);
@@ -13,7 +18,7 @@ const createMediaElement = (src, muted = false) => {
el.playsInline = true;
el.autoplay = true;
el.loop = true;
el.controls = false; // Disable native controls to prevent layout shifts
el.controls = false;
}
Object.assign(el.style, {
gridArea: "1 / 1", maxWidth: "100%", maxHeight: "80vh",
@@ -23,9 +28,11 @@ const createMediaElement = (src, muted = false) => {
return el;
};
// Video Playback Manager: Binds timeline progress bar, timeline scrubbing, and play/pause triggers
const setupVideoPlayback = (vid, container) => {
if (!vid) return;
if (!vid) return () => {};
let videoSyncActive = true;
let syncAnimationFrameId = null;
const onMetaLoaded = () => {
vid.play().catch(()=>{});
@@ -33,10 +40,9 @@ const setupVideoPlayback = (vid, container) => {
vid.addEventListener("loadedmetadata", onMetaLoaded);
if (vid.readyState >= 1) onMetaLoaded();
// Synced Scrubber Progress Bar Overlay
const controlBar = document.createElement("div");
Object.assign(controlBar.style, {
position: "absolute", bottom: "16px", left: "50%", transform: "translateX(-50%)",
position: "absolute", bottom: "16px", left: "50%",
display: "flex", alignItems: "center", gap: "12px", background: "rgba(10,10,10,0.85)",
padding: "8px 16px", borderRadius: "8px", zIndex: "40", fontSize: "11px",
fontFamily: "monospace", color: "#eee", width: "80%", maxWidth: "500px",
@@ -75,7 +81,7 @@ const setupVideoPlayback = (vid, container) => {
const scrub = (e) => {
const rect = scrubberContainer.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientX = getClientX(e);
const percent = Math.max(0, Math.min(100, (clientX - rect.left) / rect.width));
const duration = vid.duration || 0;
if (duration > 0) {
@@ -98,11 +104,9 @@ const setupVideoPlayback = (vid, container) => {
controlBar.append(playBtn, scrubberContainer, timeLabel);
container.appendChild(controlBar);
videoSyncActive = true;
const syncLoop = () => {
if (!videoSyncActive) return;
// Update progress & labels
const cur = vid.currentTime || 0;
const dur = vid.duration || 0;
if (dur > 0) {
@@ -115,14 +119,14 @@ const setupVideoPlayback = (vid, container) => {
timeLabel.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
}
// Dynamically match play button in case video loops
playBtn.className = vid.paused ? "pi pi-play" : "pi pi-pause";
syncAnimationFrameId = requestAnimationFrame(syncLoop);
};
requestAnimationFrame(syncLoop);
return () => {
videoSyncActive = false;
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
window.removeEventListener("mousemove", handleWindowMove);
window.removeEventListener("touchmove", handleWindowMove);
window.removeEventListener("mouseup", handleWindowUp);
@@ -133,14 +137,15 @@ const setupVideoPlayback = (vid, container) => {
};
function createComparisonViewer(baseSrc) {
const isBaseVideo = isVideoFormat(baseSrc);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
// Mount the viewer overlay container directly inside ComfyUI's canvas parent.
// This allows menus/sidebars outside the canvas area to naturally render on top.
const isBaseVideo = isVideoFormat(baseSrc);
const canvasEl = document.querySelector("#graph-canvas, canvas");
const targetContainer = canvasEl ? canvasEl.parentNode : document.body;
// Ensure the canvas container is positioned relatively so our overlay stretches accurately
if (targetContainer && targetContainer !== document.body) {
targetContainer.style.position = "relative";
}
@@ -151,7 +156,7 @@ function createComparisonViewer(baseSrc) {
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
background: "rgba(10, 10, 10, 0.95)", display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center",
zIndex: "10", // Above base LGraph grid but strictly below other UI components
zIndex: "10",
pointerEvents: "auto", userSelect: "none", "-webkit-user-select": "none"
});
@@ -164,11 +169,10 @@ function createComparisonViewer(baseSrc) {
const infoText = document.createElement("span");
infoText.textContent = isBaseVideo
? "Video playback. Press Esc to close."
: "Reference loaded. Click another card image in the sidebar to compare.";
: "Reference loaded. Click to replace | Shift+Click another card image to compare.";
header.appendChild(infoText);
container.appendChild(header);
// Close button
const closeBtn = document.createElement("span");
closeBtn.className = "pi pi-times";
closeBtn.title = "Close Comparison (Esc)";
@@ -180,7 +184,6 @@ function createComparisonViewer(baseSrc) {
closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
container.appendChild(closeBtn);
// Workspace wrapper holding both elements aligned pixel-perfectly
const wrapper = document.createElement("div");
Object.assign(wrapper.style, {
position: "relative", display: "grid", placeItems: "center",
@@ -188,21 +191,35 @@ function createComparisonViewer(baseSrc) {
});
container.appendChild(wrapper);
// Close strictly on clicking empty background space of the overlay backdrop
const hintPrompt = document.createElement("div");
Object.assign(hintPrompt.style, {
position: "absolute", bottom: "16px", zIndex: "30",
color: "#888", fontSize: "12px", fontFamily: "sans-serif",
pointerEvents: "none"
});
hintPrompt.innerHTML = 'Tip: Hold <span style="color:#aaa;font-weight:bold;">Shift</span> while clicking sidebar cards to compare outputs side-by-side.';
if (isBaseVideo) {
hintPrompt.style.display = "none";
}
container.appendChild(hintPrompt);
let wasDragging = false;
container.onclick = (e) => {
if (e.target === container) {
if (wasDragging) {
wasDragging = false;
return;
}
destroy();
}
};
// Media A (Base reference - Left. Created unmuted to enable audio playback)
let mediaA = createMediaElement(baseSrc, false);
wrapper.appendChild(mediaA);
// Media B (Comparison target - Right)
let mediaB = null;
// Draggable Split Slider Handle
const slider = document.createElement("div");
Object.assign(slider.style, {
position: "absolute", top: "0", bottom: "0", left: "50%",
@@ -223,7 +240,7 @@ function createComparisonViewer(baseSrc) {
slider.appendChild(sliderButton);
wrapper.appendChild(slider);
let splitRatio = 50; // Percentage
let splitRatio = 50;
let isDragging = false;
let destroyVideoPlaybackFn = null;
@@ -231,7 +248,6 @@ function createComparisonViewer(baseSrc) {
splitRatio = Math.max(0, Math.min(100, percent));
slider.style.left = `${splitRatio}%`;
if (mediaB) {
// Symmetrical split comparison layout: Image A on Left, Image B on Right
mediaB.style.clipPath = `polygon(${splitRatio}% 0, 100% 0, 100% 100%, ${splitRatio}% 100%)`;
}
};
@@ -240,23 +256,25 @@ function createComparisonViewer(baseSrc) {
e.preventDefault();
e.stopPropagation();
isDragging = true;
wasDragging = false;
// Symmetrical Click Teleportation: Snaps split-line directly to mouse coordinates
const rect = wrapper.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientX = getClientX(e);
const percent = ((clientX - rect.left) / rect.width) * 100;
updateSliderPosition(percent);
};
const doDrag = (e) => {
if (!isDragging) return;
wasDragging = true;
const rect = wrapper.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientX = getClientX(e);
const percent = ((clientX - rect.left) / rect.width) * 100;
updateSliderPosition(percent);
};
const endDrag = () => { isDragging = false; };
const endDrag = () => {
isDragging = false;
};
// Allows dragging/clicking from any region of the wrapper
wrapper.addEventListener("mousedown", startDrag);
wrapper.addEventListener("touchstart", startDrag);
@@ -265,17 +283,30 @@ function createComparisonViewer(baseSrc) {
window.addEventListener("mouseup", endDrag);
window.addEventListener("touchend", endDrag);
const cleanupMedia = (el) => {
if (el && el.tagName === "VIDEO") {
el.pause();
el.src = "";
el.load();
}
};
const destroy = () => {
window.removeEventListener("mousemove", doDrag);
window.removeEventListener("touchmove", doDrag);
window.removeEventListener("mouseup", endDrag);
window.removeEventListener("touchend", endDrag);
document.removeEventListener("keydown", handleKeys);
videoSyncActive = false;
if (syncAnimationFrameId) cancelAnimationFrame(syncAnimationFrameId);
if (globalKeydownHandler) {
document.removeEventListener("keydown", globalKeydownHandler);
globalKeydownHandler = null;
}
if (destroyVideoPlaybackFn) destroyVideoPlaybackFn();
cleanupMedia(mediaA);
cleanupMedia(mediaB);
container.remove();
activeComparisonViewer = null;
};
@@ -289,53 +320,64 @@ function createComparisonViewer(baseSrc) {
updateSliderPosition(splitRatio > 50 ? 0 : 100);
}
};
document.addEventListener("keydown", handleKeys);
globalKeydownHandler = handleKeys;
document.addEventListener("keydown", globalKeydownHandler);
targetContainer.appendChild(container);
// Initialize single-video playbacks if the base output is an MP4
if (isBaseVideo) {
destroyVideoPlaybackFn = setupVideoPlayback(mediaA, container);
}
return {
loadTarget(targetSrc) {
loadTarget(targetSrc, isShiftClick) {
const isTargetVideo = isVideoFormat(targetSrc);
if (isBaseVideo) {
// If in Video Playback Mode: clicking another item replaces the player or opens image comparison
destroy();
showFullscreenPreview([targetSrc]);
showFullscreenPreview([targetSrc], isShiftClick);
return;
}
// If in Image Comparison Mode: ignore video targets ("images compare only to other images")
if (isTargetVideo) {
return;
}
if (mediaB) mediaB.remove();
if (isShiftClick) {
if (mediaB) {
cleanupMedia(mediaB);
mediaB.remove();
}
mediaB = createMediaElement(targetSrc, false);
mediaB.style.pointerEvents = "none";
wrapper.appendChild(mediaB);
mediaB = createMediaElement(targetSrc, false);
mediaB.style.pointerEvents = "none";
wrapper.appendChild(mediaB);
slider.style.display = "block";
infoText.textContent = "Drag the slider to compare. Click other card images to update target | Esc to close.";
updateSliderPosition(50);
slider.style.display = "block";
infoText.textContent = "Drag the slider to compare. Shift+Click other card images to update target | Esc to close.";
updateSliderPosition(50);
} else {
if (mediaB) {
cleanupMedia(mediaB);
mediaB.remove();
mediaB = null;
}
slider.style.display = "none";
mediaA.src = targetSrc;
infoText.textContent = "Reference loaded. Click to replace | Shift+Click another card image to compare.";
}
}
};
}
export function showFullscreenPreview(imgSrcs) {
export function showFullscreenPreview(imgSrcs, isShiftClick = false) {
if (!imgSrcs || imgSrcs.length === 0) return;
// If the comparison overlay is already open, dynamically load the target
if (activeComparisonViewer) {
activeComparisonViewer.loadTarget(imgSrcs[0]);
activeComparisonViewer.loadTarget(imgSrcs[0], isShiftClick);
return;
}
// Otherwise, open a fresh side-by-side comparison workspace
activeComparisonViewer = createComparisonViewer(imgSrcs[0]);
}
+43 -17
View File
@@ -1,20 +1,23 @@
import { app } from "/scripts/app.js";
import { State } from "./state.js";
async function uploadDroppedImageToInput(imageObj) {
const src = `/view?filename=${encodeURIComponent(imageObj.filename)}&type=${imageObj.type || 'output'}&subfolder=${encodeURIComponent(imageObj.subfolder || '')}`;
try {
const response = await fetch(src);
if (!response.ok) return null;
const blob = await response.blob();
const file = new File([blob], imageObj.filename || "dropped_image.png", { type: blob.type });
const file = new File([blob], imageObj.filename || "dropped_file", { type: blob.type });
const formData = new FormData();
formData.append("image", file);
formData.append("overwrite", "true");
formData.append("type", "input");
const uploadRes = await fetch("/upload/image", { method: "POST", body: formData });
if (!uploadRes.ok) return null;
const uploadData = await uploadRes.json();
return uploadData.name;
} catch(e) {
console.error("Comfy Sidebar: Failed to copy image to input.", e);
console.error("Comfy Sidebar: Failed to copy file to input.", e);
return null;
}
}
@@ -26,11 +29,19 @@ export function setupDragAndDrop() {
});
document.addEventListener("drop", async (e) => {
const isSidebarDrop = e.target.closest('.comfyui-sidebar, .comfy-sidebar, [class*="sidebar"]') ||
(State.sidebarContainer && State.sidebarContainer.contains(e.target));
if (isSidebarDrop) {
e.preventDefault();
e.stopPropagation();
return;
}
const canvas = app.canvas;
if (!canvas || !canvas.graph) return;
// 1. Check for standard workflow JSON data drop
const jsonStr = e.dataTransfer.getData("application/json") || e.dataTransfer.getData("text/plain");
// 1. Check for workflow JSON drop (text-only and unfinished cards)
const jsonStr = e.dataTransfer.getData("application/json");
if (jsonStr) {
try {
const workflow = JSON.parse(jsonStr);
@@ -48,7 +59,7 @@ export function setupDragAndDrop() {
} catch (err) {}
}
// 2. Check for native image URL drop
// 2. Check for image / video URL drop
const url = e.dataTransfer.getData("text/uri-list") || e.dataTransfer.getData("text/plain");
if (url) {
try {
@@ -66,32 +77,47 @@ export function setupDragAndDrop() {
if (canvas.convertEventToCanvasOffset) {
const pos = canvas.convertEventToCanvasOffset(e);
targetNode = canvas.graph.getNodeOnPos(pos[0], pos[1]);
} else {
} else if (canvas.canvas) {
const rect = canvas.canvas.getBoundingClientRect();
targetNode = canvas.graph.getNodeOnPos((e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale, (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale);
}
if (targetNode && (targetNode.type.includes("LoadImage") || targetNode.widgets?.some(w => w.name === "image"))) {
// Dropped on a node with image input: Copy/Upload image to inputs
const widget = targetNode.widgets.find(w => w.name === "image");
if (widget) {
// Check if dropped on a LoadImage, LoadVideo, VHS_LoadVideo, or image/video widget node
const widget = targetNode?.widgets?.find(w =>
w.name === "image" || w.name === "video" || w.name === "file" || w.name === "video_path" || w.type === "image" || w.type === "customtext"
);
const isLoadNode = targetNode && (widget || targetNode.type?.includes("LoadImage") || targetNode.type?.includes("LoadVideo") || targetNode.type?.includes("VHS_LoadVideo"));
if (isLoadNode) {
const targetWidget = widget || targetNode.widgets?.[0];
if (targetWidget) {
const newFilename = await uploadDroppedImageToInput({ filename, type, subfolder });
if (newFilename) {
widget.value = newFilename;
if (widget.callback) widget.callback(widget.value);
// Populate dropdown values list so combo widgets (like VHS_LoadVideo) accept the new video immediately!
if (targetWidget.options && Array.isArray(targetWidget.options.values)) {
if (!targetWidget.options.values.includes(newFilename)) {
targetWidget.options.values.push(newFilename);
}
}
targetWidget.value = newFilename;
if (targetWidget.callback) targetWidget.callback(targetWidget.value);
targetNode.imgs = null;
app.graph.setDirtyCanvas(true, true);
return;
}
}
} else {
// Dropped on empty canvas: Load workflow natively
// Dropped on empty canvas area: fetch file and load embedded workflow metadata
const src = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
const res = await fetch(src);
const blob = await res.blob();
const file = new File([blob], filename || "workflow.png", { type: blob.type });
if (res.ok) {
const blob = await res.blob();
const file = new File([blob], filename, { type: blob.type });
if (app.handleFile) await app.handleFile(file);
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
if (app.handleFile) await app.handleFile(file);
else if (app.canvas?.handleDropItem) app.canvas.handleDropItem({ getAsFile: () => file });
}
}
}
}
+315 -169
View File
@@ -1,4 +1,3 @@
// layout.js - Fully Fixed & Renamed to 'Hide Junk'
import { app } from "/scripts/app.js";
const STYLE_ID = "comfy-sidebar-classic-layout-override";
@@ -15,7 +14,6 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
transform: none !important;
z-index: 1010 !important;
/* Nuke the background rectangle, border, and shadows */
background: transparent !important;
border: none !important;
box-shadow: none !important;
@@ -25,6 +23,45 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
align-items: center !important;
}
/* Style overrides for the new native Extensions button container when embedded in top actionbar */
.comfy-sidebar-extensions-override {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 8px 0 0 !important;
height: auto !important;
position: static !important;
order: -10 !important;
display: inline-flex !important;
align-items: center !important;
}
/* Scale down performance indicators (Crystools) uniformly */
#crysmonitor-monitors-root,
#crystools-monitors-root,
.crysmonitor-monitors-container {
zoom: 0.8 !important;
display: inline-flex !important;
align-items: center !important;
}
/* Hide user account profile button and PrimeVue components */
img[alt*="User Avatar"],
img[alt*="user avatar"],
button:has(img[alt*="User Avatar"]),
[role="button"]:has(img[alt*="User Avatar"]),
button:has(img[alt*="user avatar"]),
[role="button"]:has(img[alt*="user avatar"]),
.p-avatar,
[data-pc-name="avatar"],
button:has(.p-avatar),
[role="button"]:has(.p-avatar),
button:has([data-pc-name="avatar"]),
[role="button"]:has([data-pc-name="avatar"]) {
display: none !important;
}
/* Base rules: Large/Wide desktop screens (>= 1600px width) */
.p-tabview-nav-content,
[class*="tabview"] {
@@ -60,59 +97,106 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
}
`;
export function applyClassicLayout(enable) {
export function syncStockHistoryAndProgressSettings(enable) {
if (!app.ui?.settings) return;
const dockedVal = !!enable;
const progressVal = !enable;
const QPOV2_ID = "Comfy.Queue.QPOV2";
try {
app.ui.settings.setSettingValue(QPOV2_ID, dockedVal);
localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal));
} catch (e) {}
try {
const qpovContainer = document.querySelector('[data-setting-id="Comfy.Queue.QPOV2"]') ||
document.getElementById(QPOV2_ID)?.closest('.setting-item, tr, div');
if (qpovContainer) {
const toggle = qpovContainer.querySelector('input[type="checkbox"], [role="switch"], .p-toggleswitch');
if (toggle) {
const isChecked = toggle.checked ||
toggle.getAttribute('data-p-checked') === 'true' ||
toggle.classList.contains('p-toggleswitch-checked');
if (isChecked !== dockedVal) toggle.click();
}
}
} catch (e) {}
const progressKey = "Comfy.Queue.ShowRunProgressBar";
try {
app.ui.settings.setSettingValue(progressKey, progressVal);
localStorage.setItem(progressKey, JSON.stringify(progressVal));
} catch (e) {}
try {
const progressBarBtn = document.querySelector('[data-testid="show-run-progress-bar-action"]');
if (progressBarBtn) {
const isChecked = !!progressBarBtn.querySelector('[class*="lucide--check"], .icon-\\[lucide--check\\]');
if (isChecked !== progressVal) progressBarBtn.click();
}
} catch (e) {}
try { window.dispatchEvent(new Event("storage")); } catch (e) {}
}
export function applyClassicLayout(enable, updateSetting = false) {
let styleEl = document.getElementById(STYLE_ID);
if (enable) {
if (app.ui && app.ui.settings) {
if (updateSetting && app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
}
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = STYLE_ID;
document.head.appendChild(styleEl);
}
styleEl.textContent = CLASSIC_LAYOUT_CSS_MEDIA;
} else {
if (styleEl) styleEl.remove();
if (app.ui && app.ui.settings) {
if (updateSetting && app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
}
}
}
/* --- Properties Panel Toggle Corrections --- */
let savedButtonData = null;
let domObserver = null;
let syncScheduled = false;
function findOriginalButton() {
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
if (icon) {
const icons = document.querySelectorAll('[class*="lucide--panel-right"], [class*="lucide--panel-left"], [class*="lucide--panel-bottom"]');
for (const icon of icons) {
const btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button');
if (btn && !btn.classList.contains("comfy-sidebar-custom-properties-toggle")) {
return btn;
}
}
const buttons = document.querySelectorAll('button, [role="button"], .p-button');
for (const btn of buttons) {
if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue;
const title = (btn.getAttribute("title") || btn.getAttribute("aria-label") || "").toLowerCase();
if (title.includes("workflow overview") || title.includes("properties") || title.includes("toggle panel")) {
return btn;
}
}
return null;
}
function findTopbarContainer() {
const container = document.querySelector('[class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
const container = document.querySelector('[data-testid="action-bar-buttons"], [class*="actionbar-buttons"], .actionbar-buttons, [class*="actionbar"]');
if (container) return container;
const orig = findOriginalButton();
if (orig && orig.parentNode) return orig.parentNode;
if (orig?.parentNode) return orig.parentNode;
const buttons = document.querySelectorAll("button, .p-button");
for (const btn of buttons) {
if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue;
const title = (btn.getAttribute("title") || btn.getAttribute("aria-label") || "").toLowerCase();
if (title.includes("run") || title.includes("queue") || title.includes("manager")) {
if (title.includes("run") || title.includes("manager")) {
if (btn.parentNode) return btn.parentNode;
}
}
@@ -120,8 +204,29 @@ function findTopbarContainer() {
}
function isPropertiesPanelOpen() {
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
return !!(icon && icon.className.includes("lucide--panel-left"));
const panel = document.querySelector('[data-testid="properties-panel"]');
if (panel && panel.offsetWidth > 0 && panel.offsetHeight > 0) {
return true;
}
const headings = document.querySelectorAll('h1, h2, h3, h4');
for (const h of headings) {
if (h.textContent.trim() === "Workflow Overview" && h.offsetWidth > 0 && h.offsetHeight > 0) {
return true;
}
}
const origBtn = findOriginalButton();
if (origBtn) {
if (origBtn.classList.contains("p-button-active") ||
origBtn.classList.contains("active") ||
origBtn.getAttribute("aria-expanded") === "true" ||
origBtn.getAttribute("aria-pressed") === "true") {
return true;
}
}
return false;
}
function findActiveQueueIndicator() {
@@ -131,7 +236,6 @@ function findActiveQueueIndicator() {
const buttons = container.querySelectorAll("button, .p-button, .comfyui-menu-item");
for (const btn of buttons) {
if (btn.classList.contains("comfy-sidebar-custom-properties-toggle")) continue;
const text = (btn.textContent || "").toLowerCase();
if (text.includes("active") || text.includes("queued") || text.includes("running") || text.includes("pending")) {
return btn;
@@ -140,75 +244,59 @@ function findActiveQueueIndicator() {
return null;
}
function findNativeExtensionsPanel() {
return Array.from(document.querySelectorAll('.shadow-interface:not(.actionbar-container)'))
.find(el => el.textContent.toLowerCase().includes('extensions') || el.querySelector('button')?.textContent.toLowerCase().includes('extensions'));
}
function updateSidebarTabsVisibility() {
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
if (!sidebar) return;
// Direct mapping from tab settings to their unique class-based HTML icon tags
const tabIconSelectors = {
"Assets": '[class*="comfy--image-ai-edit"]',
"Nodes": '[class*="comfy--node"]',
"Models": '[class*="comfy--ai-model"]',
"Workflows": '[class*="comfy--workflow"]',
"Apps": '[class*="lucide--panels-top-left"]',
"NodesMap": '[class*="pi-sitemap"]',
"Templates": '[class*="comfy--template"]'
};
Object.entries(tabIconSelectors).forEach(([tab, selector]) => {
// Read from renamed "Hide Junk" path
const shouldHide = app.ui?.settings?.getSettingValue(`Comfy Sidebar.Hide Junk.${tab}`) ?? false;
const icon = sidebar.querySelector(selector);
if (icon) {
const tabBtn = icon.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || icon;
if (shouldHide) {
if (tabBtn.style.display !== "none") {
tabBtn.style.setProperty("display", "none", "important");
}
if (tabBtn.style.display !== "none") tabBtn.style.setProperty("display", "none", "important");
} else {
if (tabBtn.style.display === "none") {
tabBtn.style.removeProperty("display");
}
if (tabBtn.style.display === "none") tabBtn.style.removeProperty("display");
}
}
});
// Handle "Override Stock Job History Tab" using its renamed setting path
const hideStockHistory = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const historyIcon = sidebar.querySelector('[class*="lucide--history"]');
if (historyIcon) {
const historyBtn = historyIcon.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || historyIcon;
if (hideStockHistory) {
if (historyBtn.style.display !== "none") {
historyBtn.style.setProperty("display", "none", "important");
}
if (historyBtn.style.display !== "none") historyBtn.style.setProperty("display", "none", "important");
} else {
if (historyBtn.style.display === "none") {
historyBtn.style.removeProperty("display");
}
if (historyBtn.style.display === "none") historyBtn.style.removeProperty("display");
}
}
}
function findGraphButton() {
// 1. Target elements with Tailwind classes, but IGNORE those inside modal settings/sidebars
const elements = document.querySelectorAll('.bg-secondary-background.rounded-lg.items-center.inline-flex.pointer-events-auto');
for (const el of elements) {
// Filter out dialogs, sidebar overlays, settings drawers, and modals
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
continue;
}
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) continue;
return el;
}
// 2. Same fallback logic, excluding modals
const fallbacks = document.querySelectorAll('button, .bg-secondary-background');
for (const el of fallbacks) {
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) {
continue;
}
if (el.closest('.p-dialog, .comfy-modal, [role="dialog"], .p-sidebar, .comfy-settings')) continue;
const text = (el.textContent || el.getAttribute("title") || "").toLowerCase();
if (text.includes("graph") || text.includes("workflow") || el.querySelector('[class*="sitemap"], [class*="workflow"]')) {
return el;
@@ -217,122 +305,179 @@ function findGraphButton() {
return null;
}
function syncErrorBadge(originalBtn, customBtn) {
if (!customBtn) return;
const stockDot = document.querySelector('.comfy-sidebar-hide-original-properties-btn ~ span, .comfy-sidebar-hide-original-properties-btn + span');
let customDot = customBtn.querySelector('.comfy-sidebar-error-dot');
if (stockDot) {
if (!customDot) {
customDot = document.createElement("span");
customDot.className = "comfy-sidebar-error-dot inline-flex items-center justify-center rounded-full bg-destructive-background text-white size-2 absolute -top-1 -right-1";
customBtn.appendChild(customDot);
}
} else if (customDot) {
customDot.remove();
}
}
function cleanHTML(html) {
if (!html) return "";
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
const tempDot = tempDiv.querySelector('[class*="destructive"]');
if (tempDot) tempDot.remove();
return tempDiv.innerHTML;
}
export function syncClassicLayout() {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
if (domObserver) domObserver.disconnect();
// 1. Handle "Hide Graph Button" Toggle under the renamed "Hide Junk" path
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
const graphBtn = findGraphButton();
if (graphBtn) {
if (hideGraphBtn) {
if (graphBtn.style.display !== "none") {
graphBtn.style.setProperty("display", "none", "important");
}
} else {
if (graphBtn.style.display === "none") {
graphBtn.style.removeProperty("display");
try {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
const hideGraphBtn = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Graph Button") ?? false;
const graphBtn = findGraphButton();
if (graphBtn) {
if (hideGraphBtn) {
if (graphBtn.style.display !== "none") graphBtn.style.setProperty("display", "none", "important");
} else {
if (graphBtn.style.display === "none") graphBtn.style.removeProperty("display");
}
}
}
// 2. Exit early and clean up overrides if Classic Topbar layout is off
if (!isClassicLayoutEnabled) {
const extensionsPanel = findNativeExtensionsPanel();
if (extensionsPanel) {
if (isClassicLayoutEnabled) {
const container = findTopbarContainer();
if (container && extensionsPanel.parentNode !== container) {
if (!extensionsPanel._originalParent) {
extensionsPanel._originalParent = extensionsPanel.parentNode;
extensionsPanel._originalNextSibling = extensionsPanel.nextSibling;
}
container.appendChild(extensionsPanel);
extensionsPanel.classList.add("comfy-sidebar-extensions-override");
}
} else if (extensionsPanel._originalParent && extensionsPanel.parentNode !== extensionsPanel._originalParent) {
extensionsPanel._originalParent.insertBefore(extensionsPanel, extensionsPanel._originalNextSibling || null);
extensionsPanel.classList.remove("comfy-sidebar-extensions-override");
}
}
if (!isClassicLayoutEnabled) {
const originalBtn = findOriginalButton();
if (originalBtn?.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn");
}
const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
if (customBtn) customBtn.remove();
const indicator = findActiveQueueIndicator();
if (indicator && indicator.style.display === "none") indicator.style.removeProperty("display");
updateSidebarTabsVisibility();
return;
}
const originalBtn = findOriginalButton();
if (originalBtn && originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
originalBtn.classList.remove("comfy-sidebar-hide-original-properties-btn");
const container = findTopbarContainer();
const openState = isPropertiesPanelOpen();
if (originalBtn) {
if (!originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
originalBtn.classList.add("comfy-sidebar-hide-original-properties-btn");
}
savedButtonData = {
className: originalBtn.className.replace("comfy-sidebar-hide-original-properties-btn", "").trim(),
innerHTML: originalBtn.innerHTML,
tagName: originalBtn.tagName,
attributes: Array.from(originalBtn.attributes).map(attr => ({
name: attr.name,
value: attr.value
}))
};
}
let customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
if (!customBtn && savedButtonData && container) {
customBtn = document.createElement(savedButtonData.tagName);
customBtn.className = savedButtonData.className + " comfy-sidebar-custom-properties-toggle";
customBtn.innerHTML = cleanHTML(savedButtonData.innerHTML);
for (const attr of savedButtonData.attributes) {
if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
customBtn.setAttribute(attr.name, attr.value);
}
}
customBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const nativeBtn = findOriginalButton();
if (nativeBtn) {
nativeBtn.click();
requestAnimationFrame(() => syncClassicLayout());
}
});
container.appendChild(customBtn);
}
const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
if (customBtn) {
customBtn.remove();
if (originalBtn) {
const cleanedHTML = cleanHTML(originalBtn.innerHTML);
if (cleanedHTML && customBtn.innerHTML !== cleanedHTML) {
const existingDot = customBtn.querySelector('.comfy-sidebar-error-dot');
customBtn.innerHTML = cleanedHTML;
if (existingDot) customBtn.appendChild(existingDot);
}
const titleVal = originalBtn.getAttribute("title") || originalBtn.getAttribute("aria-label") || "Toggle properties panel";
customBtn.setAttribute("title", titleVal);
customBtn.setAttribute("aria-label", titleVal);
for (const cls of originalBtn.classList) {
if (cls !== "comfy-sidebar-hide-original-properties-btn" && !customBtn.classList.contains(cls)) {
customBtn.classList.add(cls);
}
}
syncErrorBadge(originalBtn, customBtn);
} else if (!customBtn.getAttribute("title")) {
customBtn.setAttribute("title", "Toggle properties panel");
customBtn.setAttribute("aria-label", "Toggle properties panel");
}
customBtn.classList.toggle("comfy-panel-open", openState);
customBtn.style.display = "inline-flex";
}
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const indicator = findActiveQueueIndicator();
if (indicator && indicator.style.display === "none") {
indicator.style.removeProperty("display");
}
updateSidebarTabsVisibility();
return;
}
// 3. Handle Active Layout elements synchronization
const originalBtn = findOriginalButton();
const container = findTopbarContainer();
const openState = isPropertiesPanelOpen();
if (originalBtn) {
if (!originalBtn.classList.contains("comfy-sidebar-hide-original-properties-btn")) {
originalBtn.classList.add("comfy-sidebar-hide-original-properties-btn");
}
savedButtonData = {
className: originalBtn.className.replace("comfy-sidebar-hide-original-properties-btn", "").trim(),
innerHTML: originalBtn.innerHTML,
tagName: originalBtn.tagName,
attributes: Array.from(originalBtn.attributes).map(attr => ({
name: attr.name,
value: attr.value
}))
};
}
let customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
if (!customBtn && savedButtonData && container) {
customBtn = document.createElement(savedButtonData.tagName);
customBtn.className = savedButtonData.className + " comfy-sidebar-custom-properties-toggle";
customBtn.innerHTML = savedButtonData.innerHTML;
for (const attr of savedButtonData.attributes) {
if (attr.name !== "class" && attr.name !== "id" && attr.name !== "style") {
customBtn.setAttribute(attr.name, attr.value);
}
}
Object.assign(customBtn.style, {
position: "relative",
zIndex: "1000",
margin: "0",
boxSizing: "border-box",
opacity: "1",
pointerEvents: "auto",
display: "inline-flex"
});
customBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const nativeBtn = findOriginalButton();
if (nativeBtn) nativeBtn.click();
});
container.appendChild(customBtn);
}
if (customBtn) {
customBtn.classList.toggle("comfy-panel-open", openState);
customBtn.style.display = "inline-flex";
}
// 4. Handle "0 active" indicator visibility based on history tab settings
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
const indicator = findActiveQueueIndicator();
if (indicator) {
if (hideQueueIndicator) {
if (indicator.style.display !== "none") {
indicator.style.setProperty("display", "none", "important");
}
} else {
if (indicator.style.display === "none") {
if (indicator) {
if (hideQueueIndicator) {
if (indicator.style.display !== "none") indicator.style.setProperty("display", "none", "important");
} else if (indicator.style.display === "none") {
indicator.style.removeProperty("display");
}
}
}
updateSidebarTabsVisibility();
updateSidebarTabsVisibility();
} catch (err) {
console.error("Comfy Sidebar: Error inside layout sync routine:", err);
} finally {
if (domObserver) {
domObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
}
export function setupPropertiesPanelToggleFix() {
@@ -343,8 +488,14 @@ export function setupPropertiesPanelToggleFix() {
.comfy-sidebar-hide-original-properties-btn {
display: none !important;
}
.comfy-sidebar-hide-original-properties-btn ~ span,
.comfy-sidebar-hide-original-properties-btn + span {
display: none !important;
}
.comfy-sidebar-custom-properties-toggle {
order: 99999 !important;
position: relative !important;
overflow: visible !important;
margin-left: -8px !important;
margin-right: 0px !important;
display: inline-flex !important;
@@ -353,43 +504,36 @@ export function setupPropertiesPanelToggleFix() {
padding: 0 !important;
}
.comfy-sidebar-custom-properties-toggle.comfy-panel-open {
order: 99999 !important;
margin-left: 0px !important;
margin-right: 0px !important;
}
.comfy-sidebar-custom-properties-toggle > span.comfy-sidebar-error-dot {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
}
`;
document.head.appendChild(style);
}
if (domObserver) {
domObserver.disconnect();
}
syncClassicLayout();
if (domObserver) domObserver.disconnect();
domObserver = new MutationObserver((mutations) => {
let shouldSync = false;
const isInternalSidebarMutation = (node) => {
if (!node || node.nodeType !== 1) return false;
return node.closest?.('.comfyui-sidebar, .comfy-sidebar, .comfy-sidebar-comparison-overlay') ||
node.classList?.contains('comfy-sidebar-card');
};
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
// Ensure mutation is on an actual Element node (nodeType 1) and not just text node updates
const hasElementMutation = Array.from(mutation.addedNodes).some(node => node.nodeType === 1) ||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1);
const hasExternalElementMutation =
Array.from(mutation.addedNodes).some(node => node.nodeType === 1 && !isInternalSidebarMutation(node)) ||
Array.from(mutation.removedNodes).some(node => node.nodeType === 1 && !isInternalSidebarMutation(node));
if (!hasElementMutation) continue;
// Ignore mutations on our own components to avoid feedback loops
const isOurNode = Array.from(mutation.addedNodes).some(node =>
node.classList?.contains?.("comfy-sidebar-custom-properties-toggle") ||
node.id === "comfy-sidebar-layout-fix-styles"
);
if (isOurNode) continue;
shouldSync = true;
break;
if (hasExternalElementMutation) {
shouldSync = true;
break;
}
}
}
if (shouldSync && !syncScheduled) {
@@ -405,4 +549,6 @@ export function setupPropertiesPanelToggleFix() {
childList: true,
subtree: true
});
syncClassicLayout();
}
+50 -23
View File
@@ -1,6 +1,6 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { State, promptStates, pruneHistory, loadStatesFromLocalStorage } from "./state.js";
import { State, promptStates, pruneHistory, cardElements, saveStatesToLocalStorage } from "./state.js";
import { findImagesInOutputs, findTextsInOutputs } from "./utils.js";
export let renderDOMFn = () => {};
@@ -69,6 +69,16 @@ const concludeRun = async (pid, statusStr) => {
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;
@@ -80,12 +90,14 @@ const concludeRun = async (pid, statusStr) => {
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; // Store full outputs dictionary!
if (st.images.length === 0) st.images = findImagesInOutputs(hItem[pid].outputs);
st.texts = findTextsInOutputs(hItem[pid].outputs);
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();
};
@@ -93,7 +105,6 @@ export function setupApiListeners() {
api.addEventListener("status", syncQueue);
api.addEventListener("execution_start", (e) => {
// Auto-sweep cancelled and errored jobs if setting is enabled
if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) {
const toDelete = [];
for (const [p, s] of promptStates.entries()) {
@@ -104,6 +115,7 @@ export function setupApiListeners() {
}
if (toDelete.length > 0) {
api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }).catch(()=>{});
saveStatesToLocalStorage();
}
}
@@ -123,15 +135,27 @@ export function setupApiListeners() {
workflow: activeWorkspaceWorkflow, startTime: Date.now(), duration: null
});
}
renderDOMFn(); // Render immediately to prevent pop-in delay!
renderDOMFn();
syncQueue();
});
api.addEventListener("progress", (e) => {
const pid = e.detail.prompt_id;
if (pid && promptStates.has(pid)) {
promptStates.get(pid).progress = Math.round((e.detail.value / e.detail.max) * 100);
renderDOMFn();
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();
}
}
});
@@ -155,9 +179,12 @@ export function setupApiListeners() {
const activeTasks = Array.from(promptStates.values()).filter(t => t.status === "active");
if (activeTasks.length > 0) {
const st = activeTasks[0];
if (st._previewBlobUrl) try { URL.revokeObjectURL(st._previewBlobUrl); } catch(e){}
st._previewBlobUrl = URL.createObjectURL(e.detail);
st.images = [{ url: st._previewBlobUrl }];
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();
}
});
@@ -165,18 +192,15 @@ export function setupApiListeners() {
api.addEventListener("executed", (e) => {
if (promptStates.has(e.detail.prompt_id)) {
const st = promptStates.get(e.detail.prompt_id);
const finalImgs = findImagesInOutputs({ output: e.detail.output });
const finalImgs = findImagesInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (finalImgs.length > 0) {
// Overwrite with latest images to avoid accumulation across sequential nodes
st.images = finalImgs;
}
const finalTexts = findTextsInOutputs({ output: e.detail.output });
const finalTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, st.workflow);
if (finalTexts.length > 0) {
// Overwrite with latest texts to avoid clearing previously set text outputs
st.texts = finalTexts;
}
// Dynamically capture each executed node's individual outputs
if (!st.nodeOutputs) st.nodeOutputs = {};
st.nodeOutputs[e.detail.node] = e.detail.output;
@@ -203,26 +227,29 @@ export async function initSessionAndHistory() {
}
const storedSessionId = localStorage.getItem("comfy_sidebar_backend_session_id");
if (backendSessionId && backendSessionId === storedSessionId) {
loadStatesFromLocalStorage();
} else {
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 images = findImagesInOutputs(historyData[id].outputs);
const texts = findTextsInOutputs(historyData[id].outputs);
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, // Store full outputs dictionary!
workflow: historyData[id].extra_data?.extra_pnginfo?.workflow || null,
nodeOutputs: historyData[id].outputs,
workflow: workflow,
progressText: "", timestamp: State.globalOrderCounter, rendered: true
});
});
+174 -33
View File
@@ -1,25 +1,145 @@
// sidebar.js - Fully Fixed Real-Time UI Sync (With setTimeout deferred updates)
import { app } from "/scripts/app.js";
import { injectStyles } from "./styles.js";
import { setupDragAndDrop } from "./dragdrop.js";
import { setupSidebarUI, applySidebarOverride, findOurSidebarButton, setSyncQueue, updateSidebarBadge, renderDOM } from "./ui.js";
import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies } from "./queue.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout } from "./layout.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings } from "./layout.js";
let isInitialized = false;
// Helper to scan node container for the exact text element rendering the title (Vue Mode)
function findHeaderByText(parent, text) {
if (!text) return null;
const cleanText = text.trim();
const elements = parent.querySelectorAll("span, div, h1, h2, h3, h4, p, [class*='title'], [class*='text']");
for (const el of elements) {
if (el.textContent.trim() === cleanText) {
if (el.offsetWidth > 0 && el.offsetHeight > 0 && el.offsetWidth < parent.offsetWidth * 0.9) {
return el;
}
}
}
return null;
}
function syncNodeVueBadge(node, isIgnored) {
const nodeEl = document.querySelector(`.comfy-node[data-node-id="${node.id}"], [data-node-id="${node.id}"]`);
if (!nodeEl) return;
const titleTextNode = findHeaderByText(nodeEl, node.title || node.type);
if (!titleTextNode) {
const badge = nodeEl.querySelector(".comfy-sidebar-ignore-badge");
if (badge && !isIgnored) badge.remove();
return;
}
const headerEl = titleTextNode.parentNode;
let badge = headerEl.querySelector(".comfy-sidebar-ignore-badge");
if (isIgnored) {
if (!badge) {
badge = document.createElement("div");
badge.className = "comfy-sidebar-ignore-badge";
badge.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/>
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/>
<path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/>
<line x1="2" y1="2" x2="22" y2="22"/>
</svg>
`;
Object.assign(badge.style, {
marginLeft: "auto",
marginRight: "6px",
float: "right",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "inherit",
pointerEvents: "none"
});
headerEl.appendChild(badge);
}
} else {
if (badge) badge.remove();
}
}
export function syncAllNodeBadges() {
applySidebarOverride();
if (!app.graph || !app.graph._nodes) return;
app.graph._nodes.forEach(node => {
if (!node.properties) node.properties = {};
const isIgnored = !!node.properties.ignoreInQueue;
if (isIgnored) {
if (node.boxcolor !== "#ff3333") {
node._oldBoxcolor = node.boxcolor || "";
node.boxcolor = "#ff3333";
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
} else {
if (node.boxcolor === "#ff3333") {
node.boxcolor = node._oldBoxcolor || "";
delete node._oldBoxcolor;
if (app.graph) app.graph.setDirtyCanvas(true, true);
}
}
syncNodeVueBadge(node, isIgnored);
});
}
function toggleIgnoreActiveNode() {
const canvas = app.canvas;
if (!canvas) return;
let nodes = [];
if (canvas.selected_nodes && Object.keys(canvas.selected_nodes).length > 0) {
nodes = Object.values(canvas.selected_nodes);
} else if (canvas.current_active_node) {
nodes = [canvas.current_active_node];
}
if (nodes.length === 0) return;
nodes.forEach(node => {
if (!node.properties) node.properties = {};
node.properties.ignoreInQueue = !node.properties.ignoreInQueue;
const isIgnored = !!node.properties.ignoreInQueue;
if (isIgnored) {
if (node.boxcolor !== "#ff3333") {
node._oldBoxcolor = node.boxcolor || "";
node.boxcolor = "#ff3333";
}
} else {
if (node.boxcolor === "#ff3333") {
node.boxcolor = node._oldBoxcolor || "";
delete node._oldBoxcolor;
}
}
syncNodeVueBadge(node, isIgnored);
});
if (app.graph) app.graph.setDirtyCanvas(true, true);
if (app.canvas) app.canvas.setDirty(true, true);
}
app.registerExtension({
name: "ComfySidebar.ClassicRestore",
init() {
// Standard Comfy Queue Settings
app.ui.settings.addSetting({ id: "Comfy Sidebar.Grid Columns Threshold", name: "Width Threshold for Queue Columns (px)", type: "number", defaultValue: 350 });
app.ui.settings.addSetting({ id: "Comfy Sidebar.Keep Object Aspect Ratio", name: "If disabled, cards in the queue will be cropped to the same size.", type: "boolean", defaultValue: true });
app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Pending Count Only", name: "If disabled, each queued job will have a separate individual card", type: "boolean", defaultValue: true });
app.ui.settings.addSetting({ id: "Comfy Sidebar.Show Working Node Name", name: "Shows the name of the node which is currently in the process", type: "boolean", defaultValue: true });
app.ui.settings.addSetting({ id: "Comfy Sidebar.Auto Clear Interrupted", name: "Auto-clear cancelled & failed jobs on new generation", type: "boolean", defaultValue: false });
// Add settings to hide individual sidebar tabs under the "Hide Junk" sub-category
// We use setTimeout to let Comfy finish writing the setting value before reading it
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "NodesMap", "Templates"];
const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"];
sidebarTabs.forEach(tab => {
app.ui.settings.addSetting({
id: `Comfy Sidebar.Hide Junk.${tab}`,
@@ -27,47 +147,43 @@ app.registerExtension({
type: "boolean",
defaultValue: false,
onChange: () => {
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
setTimeout(() => { syncClassicLayout(); }, 0);
}
});
});
// Group the Stock History Tab override into "Hide Junk"
app.ui.settings.addSetting({
id: "Comfy Sidebar.Hide Junk.Override Stock Job History Tab",
name: "Replaces the stock Job History sidebar with Comfy Queue",
name: "Replace the stock Job History sidebar with Comfy Queue",
type: "boolean",
defaultValue: false,
onChange: () => {
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
onChange: (value) => {
if (isInitialized && app.ui && app.ui.settings) {
syncStockHistoryAndProgressSettings(value);
}
setTimeout(() => { syncClassicLayout(); }, 0);
}
});
// Add the toggle setting for Graph Button under "Hide Junk"
app.ui.settings.addSetting({
id: "Comfy Sidebar.Hide Junk.Graph Button",
name: "Hide floating 'Graph' (Workflow/Node Map) button",
type: "boolean",
defaultValue: false,
onChange: () => {
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
setTimeout(() => { syncClassicLayout(); }, 0);
}
});
// Add the toggle setting for Classic / Comfy Layout
app.ui.settings.addSetting({
id: "Comfy Sidebar.Comfy Layout",
name: "Places the controls and the open workflow tabs on a single unified top bar",
type: "boolean",
defaultValue: false,
onChange: (value) => {
applyClassicLayout(value);
if (isInitialized) {
applyClassicLayout(value, true);
}
}
});
},
@@ -75,44 +191,69 @@ app.registerExtension({
async setup() {
if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return;
// Apply saved layout configuration on startup
const isClassicLayoutEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
applyClassicLayout(isClassicLayoutEnabled);
applyClassicLayout(isClassicLayoutEnabled, false);
const isHistoryOverrideEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
if (isHistoryOverrideEnabled) {
syncStockHistoryAndProgressSettings(true);
}
isInitialized = true;
// Run properties panel toggle correction
setupPropertiesPanelToggleFix();
// Safely resolve circular dependencies using Dependency Injection Hooks
setSyncQueue(syncQueue);
setUIDependencies(renderDOM, updateSidebarBadge);
// 1. Initialize DOM Independent Services
injectStyles();
setupDragAndDrop();
// 2. Initialize UI Hierarchy
const sidebarContainer = setupSidebarUI();
// 3. Connect API Hooks
setupApiListeners();
// 4. Connect Storage and History Sync
await initSessionAndHistory();
// 5. Connect UI Keyboard & Override Hacks
// Keyboard shortcuts listener
document.addEventListener("keydown", (e) => {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable || activeEl.tagName === "SELECT")) return;
if (e.key.toLowerCase() === "q") {
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault(); e.stopPropagation();
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.stopPropagation();
toggleIgnoreActiveNode();
}
}, true);
setInterval(() => applySidebarOverride(), 500);
// Event hooks for node additions and workflow loading (eliminates continuous polling loops)
if (app.graph) {
const originalConfigure = app.graph.configure;
app.graph.configure = function() {
const res = originalConfigure ? originalConfigure.apply(this, arguments) : undefined;
requestAnimationFrame(() => syncAllNodeBadges());
return res;
};
const originalOnNodeAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function(node) {
if (originalOnNodeAdded) originalOnNodeAdded.apply(this, arguments);
if (node && node.properties?.ignoreInQueue) {
node.boxcolor = "#ff3333";
syncNodeVueBadge(node, true);
}
};
}
// Apply sidebar override when initializing
applySidebarOverride();
// 6. Final Registration
app.extensionManager.registerSidebarTab({
id: "classic-comfy-sidebar",
icon: "pi pi-images",
+67 -15
View File
@@ -1,17 +1,37 @@
export const promptStates = new Map();
export const cardElements = new Map();
// Global shared variables
export const State = {
currentSearchQuery: "",
globalOrderCounter: 0,
sidebarContainer: null,
cardStack: null,
cardStack: null, // Main queue scroll container
submenuStack: null, // Submenu scroll container
currentlyActivePromptId: null,
activeSubmenuPromptId: null, // Active run/outputs explorer pointer
activeSubmenuBatchImages: null // Active batch images explorer pointer
};
/**
* Safely removes a prompt state from memory and revokes any active preview blob URLs.
*/
export function deletePromptState(pid) {
const state = promptStates.get(pid);
if (state) {
if (state._previewBlobUrl) {
try { URL.revokeObjectURL(state._previewBlobUrl); } catch (e) {}
}
if (state._oldPreviewBlobUrl) {
try { URL.revokeObjectURL(state._oldPreviewBlobUrl); } catch (e) {}
}
}
promptStates.delete(pid);
cardElements.delete(pid);
}
/**
* Prunes completed/cancelled history when total items exceed user setting.
*/
export function pruneHistory(app) {
const maxItems = app.ui.settings.getSettingValue("Comfy.Queue.MaxHistoryItems") ?? 64;
const tasks = Array.from(promptStates.entries())
@@ -23,36 +43,66 @@ export function pruneHistory(app) {
const deleteCount = tasks.length - maxItems;
for (let i = 0; i < deleteCount; i++) {
const [pid] = tasks[i];
promptStates.delete(pid);
cardElements.delete(pid);
deletePromptState(pid);
}
}
}
/**
* Saves all prompt states (including workflows, text outputs, video runs, and failed tasks) to localStorage.
* Call ONLY on job completion, cancellation, error, or card deletion.
*/
export function saveStatesToLocalStorage() {
try {
const serializable = [];
let serializable = [];
for (const [pid, state] of promptStates.entries()) {
const cleanedImages = (state.images || []).map(img => {
if (img.url && img.url.startsWith("blob:")) return null; // Skip blob URLs
if (img.url && img.url.startsWith("blob:")) return null;
return img;
}).filter(Boolean);
serializable.push({
pid: state.pid, status: state.status, images: cleanedImages, texts: state.texts || [],
nodeOutputs: state.nodeOutputs, // Persist node level outputs
workflow: state.workflow, progress: state.progress || 0, queueNumber: state.queueNumber,
progressText: state.progressText || "", timestamp: state.timestamp,
activeNodeName: state.activeNodeName || "", rendered: state.rendered || false,
startTime: state.startTime, endTime: state.endTime, duration: state.duration
pid: state.pid,
status: state.status,
images: cleanedImages,
texts: state.texts || [],
nodeOutputs: state.nodeOutputs,
workflow: state.workflow,
progress: state.progress || 0,
queueNumber: state.queueNumber,
progressText: state.progressText || "",
timestamp: state.timestamp,
activeNodeName: state.activeNodeName || "",
rendered: state.rendered || false,
startTime: state.startTime,
endTime: state.endTime,
duration: state.duration
});
}
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
serializable.sort((a, b) => a.timestamp - b.timestamp);
while (serializable.length > 0) {
try {
localStorage.setItem("comfy_sidebar_prompt_states", JSON.stringify(serializable));
break;
} catch (e) {
if (e.name === "QuotaExceededError" || e.code === 22 || e.code === 1014) {
serializable.shift();
} else {
console.error("Comfy Sidebar: Failed to save state to localStorage", e);
break;
}
}
}
} catch (e) {
console.error("Comfy Sidebar: Failed to save state to localStorage", e);
console.error("Comfy Sidebar: Error preparing localStorage data", e);
}
}
/**
* Loads cached card states on extension initialization.
*/
export function loadStatesFromLocalStorage() {
try {
const data = localStorage.getItem("comfy_sidebar_prompt_states");
@@ -66,4 +116,6 @@ export function loadStatesFromLocalStorage() {
} catch (e) {
console.error("Comfy Sidebar: Failed to load state from localStorage", e);
}
}
}
loadStatesFromLocalStorage();
+39 -8
View File
@@ -1,6 +1,7 @@
// styles.js
export function injectStyles() {
if (document.getElementById("comfy-sidebar-styles")) return;
const style = document.createElement("style");
style.id = "comfy-sidebar-styles";
style.textContent = `
.comfy-sidebar-card {
background: var(--comfy-input-bg, #181818);
@@ -32,29 +33,59 @@ export function injectStyles() {
.comfy-sidebar-card-timer {
position: absolute; top: 6px; left: 8px; font-size: 10px;
font-family: monospace; opacity: 0.7; background: rgba(0, 0, 0, 0.6);
padding: 2px 4px; border-radius: 3px; pointer-events: none; z-index: 5; color: #fff;
padding: 2px 4px; border-radius: 3px; pointerEvents: none; z-index: 5; color: #fff;
}
/* --- Custom Queue Cancel Button to Mimic ComfyUI Native Destructive Button --- */
.comfy-sidebar-queue-cancel-btn {
display: none;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
background-color: #7f1d1d !important; /* Native deep red */
color: #e2e8f0 !important; /* Native slate white icon */
font-size: 11px !important; /* Muted native cross size */
border-radius: 6px !important; /* Native corner rounding */
background-color: #7f1d1d !important;
color: #e2e8f0 !important;
font-size: 11px !important;
border-radius: 6px !important;
cursor: pointer !important;
transition: background-color 0.15s ease, color 0.15s ease !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
}
.comfy-sidebar-queue-cancel-btn:hover {
background-color: #991b1b !important; /* Slightly brighter red on hover */
background-color: #991b1b !important;
color: #ffffff !important;
}
/* Card Action Buttons */
.comfy-sidebar-card-action-btn {
display: inline-flex;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
background-color: rgba(0, 0, 0, 0.75) !important;
color: #e2e8f0 !important;
font-size: 14px !important;
border-radius: 6px !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
z-index: 20 !important;
transform: translateZ(0);
}
.comfy-sidebar-card-action-btn:hover {
background-color: rgba(0, 0, 0, 0.95) !important;
color: #ffffff !important;
}
/* Delete Confirmation Active State */
.comfy-sidebar-card-action-btn.confirm-delete,
.comfy-sidebar-card-action-btn.confirm-delete:hover {
background-color: #dc3545 !important;
color: #ffffff !important;
box-shadow: 0 0 8px rgba(220, 53, 69, 0.6) !important;
}
`;
document.head.appendChild(style);
}
+539 -315
View File
File diff suppressed because it is too large Load Diff
+21 -4
View File
@@ -1,13 +1,29 @@
import { app } from "/scripts/app.js";
export const isVideoFormat = (url) => {
const s = url.toLowerCase();
return s.includes(".mp4") || s.includes(".webm");
};
function isNodeIgnored(nodeId, workflow) {
if (!workflow || !workflow.nodes) {
// Fallback to active graph if workflow is missing (e.g. during live execution)
const liveNode = app.graph?.getNodeById(Number(nodeId));
if (liveNode) {
return !!(liveNode.properties && liveNode.properties.ignoreInQueue);
}
return false;
}
const node = workflow.nodes.find(n => String(n.id) === String(nodeId));
return !!(node && node.properties && node.properties.ignoreInQueue);
}
// Robust media output scanner supporting both arrays and single objects (gifs, mp4s, etc.)
export function findImagesInOutputs(outputs) {
export function findImagesInOutputs(outputs, workflow) {
const list = [];
if (!outputs) return list;
for (const nodeId in outputs) {
if (isNodeIgnored(nodeId, workflow)) continue;
for (const key in outputs[nodeId]) {
const val = outputs[nodeId][key];
if (Array.isArray(val)) {
@@ -22,10 +38,11 @@ export function findImagesInOutputs(outputs) {
return list;
}
export function findTextsInOutputs(outputs) {
export function findTextsInOutputs(outputs, workflow) {
const list = [];
if (!outputs) return list;
for (const nodeId in outputs) {
if (isNodeIgnored(nodeId, workflow)) continue;
for (const key in outputs[nodeId]) {
const val = outputs[nodeId][key];
if (Array.isArray(val)) {
@@ -49,11 +66,11 @@ export function findTextsInOutputs(outputs) {
}
// Organizes raw outputs dictionaries into lists of individual node-level output blocks (ignoring text-only outputs)
export function getRunOutputs(nodeOutputs) {
export function getRunOutputs(nodeOutputs, workflow) {
const list = [];
if (!nodeOutputs) return list;
for (const nodeId in nodeOutputs) {
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] });
const imgs = findImagesInOutputs({ [nodeId]: nodeOutputs[nodeId] }, workflow);
if (imgs.length > 0) {
list.push({ nodeId, images: imgs });
}
+13
View File
@@ -0,0 +1,13 @@
[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.2.9"
license = { file = "LICENSE" }
dependencies = []
[project.urls]
Repository = "https://github.com/m0rtus59/ComfyUI-ComfySidebar"
[tool.comfy]
PublisherId = "soundslikethunder"
DisplayName = "Comfy Sidebar"