Compare commits

...
5 Commits
8 changed files with 443 additions and 169 deletions
+5 -2
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 shift+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.
* **`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.
+10
View File
@@ -1,4 +1,5 @@
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 || '')}`;
@@ -26,6 +27,15 @@ export function setupDragAndDrop() {
});
document.addEventListener("drop", async (e) => {
// Cancel loading entirely and prevent bubbling if dropping inside the sidebar area
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;
+197 -117
View File
@@ -25,6 +25,46 @@ 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 without breaking their native styling */
#crysmonitor-monitors-root,
#crystools-monitors-root,
.crysmonitor-monitors-container {
zoom: 0.8 !important; /* Restored to original 0.8 scale for perfect topbar alignment */
display: inline-flex !important;
align-items: center !important;
}
/* Hide only the user account profile button and its PrimeVue components (initials/icons) */
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"]),
/* PrimeVue Avatar component class and attribute selectors */
.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,11 +100,11 @@ const CLASSIC_LAYOUT_CSS_MEDIA = `
}
`;
export function applyClassicLayout(enable) {
export function applyClassicLayout(enable, updateSetting = false) {
let styleEl = document.getElementById(STYLE_ID);
if (enable) {
if (app.ui && app.ui.settings) {
if (updateSetting && app.ui && app.ui.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
}
@@ -78,7 +118,7 @@ export function applyClassicLayout(enable) {
} else {
if (styleEl) styleEl.remove();
if (app.ui && app.ui.settings) {
if (updateSetting && app.ui && app.ui.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
}
}
@@ -102,7 +142,7 @@ function findOriginalButton() {
}
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();
@@ -121,7 +161,9 @@ function findTopbarContainer() {
function isPropertiesPanelOpen() {
const icon = document.querySelector('[class*="lucide--panel-right"], [class*="lucide--panel-left"]');
return !!(icon && icon.className.includes("lucide--panel-left"));
if (!icon) return false;
const classStr = icon.getAttribute("class") || "";
return classStr.includes("lucide--panel-left");
}
function findActiveQueueIndicator() {
@@ -140,6 +182,12 @@ function findActiveQueueIndicator() {
return null;
}
function findNativeExtensionsPanel() {
// Exclude the main actionbar-container itself to prevent infinite loop appending bugs
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;
@@ -218,121 +266,160 @@ function findGraphButton() {
}
export function syncClassicLayout() {
const isClassicLayoutEnabled = app.ui?.settings?.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
// Temporarily disconnect observer to cleanly avoid infinite layout feedback loops during DOM rearrangement
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;
// 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");
}
}
}
}
// 2. Exit early and clean up overrides if Classic Topbar layout is off
if (!isClassicLayoutEnabled) {
// 2. Handle Native Extensions Button alignment in Classic Layout
const extensionsPanel = findNativeExtensionsPanel();
if (extensionsPanel) {
if (isClassicLayoutEnabled) {
const container = findTopbarContainer();
if (container && extensionsPanel.parentNode !== container) {
// Save original parent so we can restore it if the layout toggle is turned off
if (!extensionsPanel._originalParent) {
extensionsPanel._originalParent = extensionsPanel.parentNode;
extensionsPanel._originalNextSibling = extensionsPanel.nextSibling;
}
container.appendChild(extensionsPanel);
extensionsPanel.classList.add("comfy-sidebar-extensions-override");
}
} else {
// Restore original placement when classic layout is disabled
if (extensionsPanel._originalParent && extensionsPanel.parentNode !== extensionsPanel._originalParent) {
extensionsPanel._originalParent.insertBefore(extensionsPanel, extensionsPanel._originalNextSibling || null);
extensionsPanel.classList.remove("comfy-sidebar-extensions-override");
}
}
}
// 3. Exit early and clean up overrides if Classic Topbar layout is off
if (!isClassicLayoutEnabled) {
const originalBtn = findOriginalButton();
if (originalBtn && 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;
}
// 4. Handle Active Layout elements synchronization
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 = 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);
}
const customBtn = document.querySelector(".comfy-sidebar-custom-properties-toggle");
if (customBtn) {
customBtn.remove();
customBtn.classList.toggle("comfy-panel-open", openState);
customBtn.style.display = "inline-flex";
}
// 5. 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 && indicator.style.display === "none") {
indicator.style.removeProperty("display");
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();
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") {
indicator.style.removeProperty("display");
}
} catch (err) {
console.error("Comfy Sidebar: Error inside layout sync routine:", err);
} finally {
// Re-enable mutation monitoring once our changes have successfully updated in the DOM
if (domObserver) {
domObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
updateSidebarTabsVisibility();
}
export function setupPropertiesPanelToggleFix() {
@@ -369,27 +456,17 @@ export function setupPropertiesPanelToggleFix() {
domObserver.disconnect();
}
syncClassicLayout();
domObserver = new MutationObserver((mutations) => {
let shouldSync = false;
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);
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 (hasElementMutation) {
shouldSync = true;
break;
}
}
}
if (shouldSync && !syncScheduled) {
@@ -405,4 +482,7 @@ export function setupPropertiesPanelToggleFix() {
childList: true,
subtree: true
});
// Run first layout sync now that structural observing is active
syncClassicLayout();
}
+15 -11
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, loadStatesFromLocalStorage, cardElements } from "./state.js";
import { findImagesInOutputs, findTextsInOutputs } from "./utils.js";
export let renderDOMFn = () => {};
@@ -81,8 +81,8 @@ const concludeRun = async (pid, statusStr) => {
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);
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);
@@ -165,12 +165,12 @@ 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;
@@ -203,26 +203,30 @@ export async function initSessionAndHistory() {
}
const storedSessionId = localStorage.getItem("comfy_sidebar_backend_session_id");
if (backendSessionId && backendSessionId === storedSessionId) {
loadStatesFromLocalStorage();
} else {
if (backendSessionId && backendSessionId !== storedSessionId) {
// Session changed (server restarted)! Clear cached memory.
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,
workflow: workflow,
progressText: "", timestamp: State.globalOrderCounter, rendered: true
});
});
+144 -7
View File
@@ -6,6 +6,48 @@ import { setupSidebarUI, applySidebarOverride, findOurSidebarButton, setSyncQueu
import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies } from "./queue.js";
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout } from "./layout.js";
// Track initial loading phase to guard against early auto-fired settings triggers
let isInitialized = false;
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;
});
app.graph.setDirtyCanvas(true, true);
}
// 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.replace(/^(🚫|👁️⃠)\s*/, "").trim();
// Grab all potential elements that could hold the title text
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) {
// Ensure we are selecting a small title element, not the entire node wrapper
if (el.offsetWidth > 0 && el.offsetHeight > 0 && el.offsetWidth < parent.offsetWidth * 0.9) {
return el;
}
}
}
return null;
}
app.registerExtension({
name: "ComfySidebar.ClassicRestore",
@@ -44,8 +86,8 @@ app.registerExtension({
setTimeout(() => {
if (typeof syncClassicLayout === "function") syncClassicLayout();
}, 0);
}
});
}
});
// Add the toggle setting for Graph Button under "Hide Junk"
app.ui.settings.addSetting({
@@ -67,7 +109,10 @@ app.registerExtension({
type: "boolean",
defaultValue: false,
onChange: (value) => {
applyClassicLayout(value);
// Only update the native layout configuration if setup() initialization has fully concluded
if (isInitialized) {
applyClassicLayout(value, true);
}
}
});
},
@@ -75,9 +120,12 @@ app.registerExtension({
async setup() {
if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return;
// Apply saved layout configuration on startup
// Apply saved layout configuration on startup (never force overwrite on launch)
const isClassicLayoutEnabled = app.ui.settings.getSettingValue("Comfy Sidebar.Comfy Layout") ?? false;
applyClassicLayout(isClassicLayoutEnabled);
applyClassicLayout(isClassicLayoutEnabled, false);
// Finalize initialization to open write pathways for settings-panel adjustments
isInitialized = true;
// Run properties panel toggle correction
setupPropertiesPanelToggleFix();
@@ -103,14 +151,103 @@ app.registerExtension({
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") {
// Toggle Queue open/close (Q)
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault(); e.stopPropagation();
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
// Toggle Ignore Node in Queue (Ctrl + Q)
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.stopPropagation();
toggleIgnoreActiveNode();
}
}, true);
setInterval(() => applySidebarOverride(), 500);
setInterval(() => {
applySidebarOverride();
// Sync ignoreInQueue visual states across both canvas and Vue DOM modes dynamically
if (app.graph && app.graph._nodes) {
app.graph._nodes.forEach(node => {
if (!node.properties) node.properties = {};
const isIgnored = !!node.properties.ignoreInQueue;
// 1. Sweep away legacy title emoji residue to keep graph titles perfectly clean
if (node.title && (node.title.startsWith("🚫 ") || node.title.startsWith("👁️⃠ "))) {
node.title = node.title.replace(/^(🚫|👁️⃠)\s*/, "");
app.graph.setDirtyCanvas(true, true);
}
// 2. Classic LiteGraph Native Glowing Border Sync
if (isIgnored) {
if (node.boxcolor !== "#ff3333") {
node._oldBoxcolor = node.boxcolor || "";
node.boxcolor = "#ff3333";
app.graph.setDirtyCanvas(true, true);
}
} else {
if (node.boxcolor === "#ff3333") {
node.boxcolor = node._oldBoxcolor || "";
delete node._oldBoxcolor;
app.graph.setDirtyCanvas(true, true);
}
}
// 3. Nodes 2.0 DOM Sibling Support (Vue Mode)
const nodeEl = document.querySelector(`.comfy-node[data-node-id="${node.id}"], [data-node-id="${node.id}"]`);
if (nodeEl) {
// Dynamically locate the node header container by searching for the node title text element
const titleTextNode = findHeaderByText(nodeEl, node.title || node.type);
if (titleTextNode) {
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";
// Custom SVG crossed-out eye
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>
`;
// Flows natively inside the parent header block with automatic flex alignment
Object.assign(badge.style, {
marginLeft: "auto",
marginRight: "6px",
float: "right",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "inherit", // Inherits color of parent text (automatically morphs on highlight!)
pointerEvents: "none"
});
headerEl.appendChild(badge);
}
} else {
if (badge) {
badge.remove();
}
}
} else {
// Fallback cleanup if the title text element is temporarily unmounted
const badge = nodeEl.querySelector(".comfy-sidebar-ignore-badge");
if (badge && !isIgnored) {
badge.remove();
}
}
}
});
}
}, 500);
// 6. Final Registration
app.extensionManager.registerSidebarTab({
+4 -1
View File
@@ -66,4 +66,7 @@ export function loadStatesFromLocalStorage() {
} catch (e) {
console.error("Comfy Sidebar: Failed to load state from localStorage", e);
}
}
}
// Load instantly and synchronously on script import to prevent race conditions during early renders!
loadStatesFromLocalStorage();
+47 -27
View File
@@ -202,7 +202,7 @@ export function setupSidebarUI() {
promptStates.delete(pid);
}
}
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clear: true }) }); } catch (err) {}
try { await api.fetchApi("/history", { method: "POST", body: JSON.stringify({ clear: true }) }); } catch (err) {}
renderDOM();
});
@@ -336,6 +336,7 @@ function renderCardImages(cardObj, state, keepAspect) {
}
};
// Bind dimensions hooks and event listeners EVERY run (for recycled elements & fast cache hits!)
if (isVideo) {
mediaEl.muted = true;
mediaEl.playsInline = true;
@@ -346,6 +347,9 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.onloadedmetadata = () => {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
};
if (mediaEl.readyState >= 1) {
applyDimensions(mediaEl.videoWidth, mediaEl.videoHeight);
}
let playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (!playIcon) {
@@ -367,26 +371,39 @@ function renderCardImages(cardObj, state, keepAspect) {
mediaEl.onmouseenter = () => { playIcon.style.opacity = "0"; mediaEl.play().catch(()=>{}); };
mediaEl.onmouseleave = () => { playIcon.style.opacity = "1"; mediaEl.pause(); };
} else {
mediaEl.setAttribute("draggable", "true");
const isUnfinished = state.status && state.status !== "completed";
if (isUnfinished) {
mediaEl.setAttribute("draggable", "false");
mediaEl.style.cursor = "grab";
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
delete mediaEl._currentDragStart;
} else {
mediaEl.setAttribute("draggable", "true");
mediaEl.style.cursor = "zoom-in";
const dragStartHandler = (e) => {
State.currentDraggedImgData = { ...img, workflow: state.workflow };
try {
e.dataTransfer.setData("text/uri-list", src);
e.dataTransfer.setData("text/plain", src);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
e.stopPropagation();
};
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
mediaEl.addEventListener("dragstart", dragStartHandler);
mediaEl._currentDragStart = dragStartHandler;
}
mediaEl.onload = () => {
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
};
if (mediaEl.complete && mediaEl.naturalWidth) {
applyDimensions(mediaEl.naturalWidth, mediaEl.naturalHeight);
}
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
if (playIcon) playIcon.remove();
const dragStartHandler = (e) => {
State.currentDraggedImgData = { ...img, workflow: state.workflow };
try {
e.dataTransfer.setData("text/uri-list", src);
e.dataTransfer.setData("text/plain", src);
} catch (err) {}
e.dataTransfer.effectAllowed = "copy";
e.stopPropagation();
};
mediaEl.removeEventListener("dragstart", mediaEl._currentDragStart);
mediaEl.addEventListener("dragstart", dragStartHandler);
mediaEl._currentDragStart = dragStartHandler;
}
// Smooth in-memory source swapping for blob live previews
@@ -604,7 +621,7 @@ export function renderDOM() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.title = "Show Node"; // Renamed
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
leftHoverPanel.appendChild(btnFocus);
@@ -683,7 +700,7 @@ export function renderDOM() {
globalClickRegistered = true;
}
const outputs = getRunOutputs(st.nodeOutputs);
const outputs = getRunOutputs(st.nodeOutputs, st.workflow);
const targetElements = [];
outputs.forEach((out) => {
@@ -740,7 +757,7 @@ export function renderDOM() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.title = "Show Node"; // Renamed
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
leftHoverPanel.appendChild(btnFocus);
@@ -865,7 +882,7 @@ export function renderDOM() {
const btnFocus = document.createElement("span");
btnFocus.className = "pi pi-eye";
btnFocus.title = "Show Node"; // Renamed
btnFocus.title = "Show Node";
styleActionBtn(btnFocus);
const leftHoverBtn = document.createElement("span");
@@ -909,9 +926,10 @@ export function renderDOM() {
cardObj.btnFocus.style.display = "none";
}
// Show explorer view button if this run yielded multiple node outputs
const outputs = getRunOutputs(state.nodeOutputs);
if (outputs.length > 1) {
// Show explorer view button if this run yielded multiple node outputs, or if it was interrupted/failed but has some outputs
const outputs = getRunOutputs(state.nodeOutputs, state.workflow);
const isInterrupted = state.status === "cancelled" || state.status === "error";
if (outputs.length > 1 || (outputs.length > 0 && isInterrupted)) {
cardObj.leftHoverBtn.style.display = "inline-flex";
} else {
cardObj.leftHoverBtn.style.display = "none";
@@ -924,9 +942,10 @@ export function renderDOM() {
});
card.addEventListener("dragstart", (e) => {
// Custom dragstart is only used for JSON-only workflows to supply a local DownloadURL.
// For image cards, dragging is handled natively by the browser on the child <img> tag.
if (state.workflow && (!state.images || state.images.length === 0)) {
// Custom dragstart is used for JSON-only or unfinished workflows to supply a local DownloadURL.
// For completed image cards, dragging is handled natively by the browser on the child <img> tag.
const isUnfinished = state.status && state.status !== "completed";
if (state.workflow && (!state.images || state.images.length === 0 || isUnfinished)) {
if (cardObj.firstImgElement) e.dataTransfer.setDragImage(cardObj.firstImgElement, 15, 15);
const jsonStr = JSON.stringify(state.workflow, null, 2);
try {
@@ -943,9 +962,10 @@ export function renderDOM() {
cardObj.element.className = `comfy-sidebar-card ${state.status}`;
// Native drag isolation: completely remove draggable flags on image containers.
// Native drag isolation: completely remove draggable flags on image containers for finished cards.
// This exposes the child <img> tag directly to Chrome's native filesystem drag APIs.
if (state.images && state.images.length > 0) {
const isUnfinished = state.status && state.status !== "completed";
if (state.images && state.images.length > 0 && !isUnfinished) {
cardObj.element.removeAttribute("draggable");
} else {
cardObj.element.setAttribute("draggable", "true");
+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 });
}