Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3c3cb598 | ||
|
|
0238cf109d | ||
|
|
869d3b9a28 | ||
|
|
f09e2c7921 | ||
|
|
9adfb2a53f |
@@ -16,9 +16,10 @@ A compact sidebar for ComfyUI that replaces the stock queue/history UI with a fa
|
||||
* Hide sidebar tabs and UI elements you don't use.
|
||||
* Optionally replace the stock Job History sidebar.
|
||||
* Optional unified top bar for workflow tabs and controls.
|
||||
* Exclude selected nodes from sidebar results with `Ctrl+Q`.
|
||||
* [NEW] Ctrl-clicking the sidebar cards opens their file location or the menu for alternative actions.
|
||||
* [NEW] Ctrl-clicking the delete button places the generated file to the recycle bin.
|
||||
* Exclude selected nodes from sidebar results with `Ctrl+Q` (or the header button).
|
||||
* Ctrl-clicking the sidebar cards opens their file location or the menu for alternative actions.
|
||||
* Ctrl-clicking the delete button places the generated file to the recycle bin.
|
||||
* [NEW] Reset all node IDs in topological execution order (`Alt+R`, right-click menu, or header button) to ensure the final output node is always prioritized as the primary preview.
|
||||
|
||||
<img width="385" height="336" alt="image" src="https://github.com/user-attachments/assets/743fbf85-9281-4561-9dd0-cd9af4d8c0e4" />
|
||||
|
||||
@@ -55,6 +56,7 @@ Open **Settings → Comfy Sidebar** to configure:
|
||||
| ------------- | -------------------------------------------------- |
|
||||
| `Q` | Toggle ComfySidebar |
|
||||
| `Ctrl+Q` | Exclude the selected node from the sidebar outputs |
|
||||
| `Alt+R` | Reset all node IDs by execution order |
|
||||
| `Click` | Open result preview |
|
||||
| `Shift+Click` | Compare another result |
|
||||
| `Ctrl+Click` | Open file location/alternative action |
|
||||
|
||||
+17
-4
@@ -16,7 +16,12 @@ def is_path_safe(base_dir, target_path):
|
||||
"""Prevents Path Traversal attacks."""
|
||||
real_base = os.path.realpath(base_dir)
|
||||
real_target = os.path.realpath(target_path)
|
||||
return real_target == real_base or real_target.startswith(real_base + os.sep)
|
||||
try:
|
||||
# commonpath reliably handles OS-specific case sensitivity and slash logic
|
||||
return os.path.commonpath([real_base, real_target]) == real_base
|
||||
except ValueError:
|
||||
# Raised if paths are on different drives on Windows
|
||||
return False
|
||||
|
||||
|
||||
def get_all_candidate_roots(subfolder="", folder_type="output"):
|
||||
@@ -199,9 +204,16 @@ async def three_proxy_handler(request):
|
||||
|
||||
os.makedirs(SIDEBAR_CACHE_DIR, exist_ok=True)
|
||||
import re
|
||||
safe_name = re.sub(r'[^a-zA-Z0-9_.-]', '_', full_req)
|
||||
if not any(safe_name.endswith(ext) for ext in [".js", ".mjs", ".wasm", ".bin", ".json"]):
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
|
||||
# Hash the request to prevent Cache Poisoning collisions and OS filename length limits
|
||||
req_hash = hashlib.sha256(full_req.encode("utf-8")).hexdigest()[:16]
|
||||
prefix = re.sub(r'[^a-zA-Z0-9.-]', '_', raw_path)[:40]
|
||||
safe_name = f"{prefix}_{req_hash}"
|
||||
if not any(raw_path.endswith(ext) for ext in [".js", ".mjs", ".wasm", ".bin", ".json"]):
|
||||
safe_name += ".js"
|
||||
|
||||
cache_file = os.path.join(SIDEBAR_CACHE_DIR, safe_name)
|
||||
|
||||
# 1. Serve immediately from disk cache if present
|
||||
@@ -210,7 +222,8 @@ async def three_proxy_handler(request):
|
||||
return web.FileResponse(cache_file, headers={"Content-Type": content_type})
|
||||
|
||||
# 2. Otherwise download via Python (not blocked by browser CSP)
|
||||
url = f"https://esm.sh/{full_req}"
|
||||
safe_url_path = urllib.parse.quote(full_req, safe="?&=/@+.-")
|
||||
url = f"https://esm.sh/{safe_url_path}"
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
|
||||
|
||||
+31
-22
@@ -103,18 +103,30 @@ export function findGraphButton() {
|
||||
}
|
||||
|
||||
export function findOurSidebarButton() {
|
||||
const byTestId = document.querySelector('[data-testid="classic-comfy-sidebar-tab-button"]');
|
||||
if (byTestId) return byTestId;
|
||||
|
||||
const icon = document.querySelector('.pi-images');
|
||||
return icon ? icon.closest('.comfyui-sidebar-tab, button, [role="tab"]') : null;
|
||||
}
|
||||
|
||||
export function findStandardQueueButton() {
|
||||
for (const iconSelector of [".pi-history", ".pi-clock", ".pi-server", ".pi-list", ".pi-sliders-h"]) {
|
||||
// 1. Try modern ComfyUI stable test-ids first
|
||||
const byTestId = document.querySelector('[data-testid="queue-tab-button"], [data-testid="job-history-tab-button"]');
|
||||
if (byTestId && !byTestId.id?.includes('classic-comfy-sidebar') && !byTestId.querySelector('.pi-images')) {
|
||||
return byTestId;
|
||||
}
|
||||
|
||||
// 2. Fallback to icon discovery
|
||||
for (const iconSelector of [".pi-history", ".pi-clock", ".pi-server", ".pi-list", ".pi-sliders-h", '[class*="lucide--history"]']) {
|
||||
const icon = document.querySelector(iconSelector);
|
||||
if (icon) {
|
||||
const btn = icon.closest('.comfyui-sidebar-tab, button, [role="tab"]');
|
||||
if (btn && !btn.querySelector('.pi-images')) return btn;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to semantic labels
|
||||
const buttons = document.querySelectorAll('.comfyui-sidebar-tab, button, [role="tab"]');
|
||||
for (const btn of buttons) {
|
||||
const title = btn.title || btn.getAttribute('aria-label') || '';
|
||||
@@ -128,29 +140,26 @@ export function findStandardQueueButton() {
|
||||
}
|
||||
|
||||
export function updateSidebarBadge(count) {
|
||||
const icons = document.querySelectorAll('.pi-images');
|
||||
icons.forEach(icon => {
|
||||
const btn = icon.closest('.comfyui-sidebar-tab, button, [role="tab"]');
|
||||
if (btn) {
|
||||
let badge = btn.querySelector('.comfy-sidebar-badge');
|
||||
if (count > 0) {
|
||||
if (!badge) {
|
||||
btn.style.position = 'relative';
|
||||
badge = document.createElement('div');
|
||||
badge.className = 'comfy-sidebar-badge';
|
||||
Object.assign(badge.style, {
|
||||
position: 'absolute', top: '2px', right: '2px', background: '#0ea5e9', color: '#fff',
|
||||
borderRadius: '50%', width: '16px', height: '16px', fontSize: '10px', display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', pointerEvents: 'none'
|
||||
});
|
||||
btn.appendChild(badge);
|
||||
}
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
} else if (badge) {
|
||||
badge.remove();
|
||||
const btn = findOurSidebarButton();
|
||||
if (btn) {
|
||||
let badge = btn.querySelector('.comfy-sidebar-badge');
|
||||
if (count > 0) {
|
||||
if (!badge) {
|
||||
btn.style.position = 'relative';
|
||||
badge = document.createElement('div');
|
||||
badge.className = 'comfy-sidebar-badge';
|
||||
Object.assign(badge.style, {
|
||||
position: 'absolute', top: '2px', right: '2px', background: '#0ea5e9', color: '#fff',
|
||||
borderRadius: '50%', width: '16px', height: '16px', fontSize: '10px', display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', pointerEvents: 'none'
|
||||
});
|
||||
btn.appendChild(badge);
|
||||
}
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
} else if (badge) {
|
||||
badge.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function applySidebarOverride() {
|
||||
|
||||
+151
-143
@@ -163,20 +163,21 @@ function updateSidebarTabsVisibility() {
|
||||
const sidebar = document.querySelector('.comfyui-sidebar, .comfy-sidebar, .sidebar, [class*="sidebar-nav"], [class*="sidebar"]');
|
||||
if (!sidebar) return;
|
||||
|
||||
const tabIconSelectors = {
|
||||
"Assets": '[class*="comfy--image-ai-edit"]',
|
||||
"Nodes": '[class*="comfy--node"]',
|
||||
"Models": '[class*="comfy--ai-model"]',
|
||||
"Workflows": '[class*="comfy--workflow"], [class*="workflow"]',
|
||||
"Apps": '[class*="lucide--panels-top-left"]',
|
||||
"Templates": '[class*="comfy--template"]'
|
||||
// Use stable ComfyUI data-testid selectors first, with icon classes as safe fallback
|
||||
const tabSelectors = {
|
||||
"Assets": '[data-testid="assets-tab-button"], [class*="comfy--image-ai-edit"]',
|
||||
"Nodes": '[data-testid="node-library-tab-button"], [class*="comfy--node"]',
|
||||
"Models": '[data-testid="model-library-tab-button"], [class*="comfy--ai-model"]',
|
||||
"Workflows": '[data-testid="workflows-tab-button"], [class*="comfy--workflow"], [class*="workflow"]',
|
||||
"Apps": '[data-testid="apps-tab-button"], [class*="lucide--panels-top-left"]',
|
||||
"Templates": '[data-testid="templates-tab-button"], [class*="comfy--template"]'
|
||||
};
|
||||
|
||||
Object.entries(tabIconSelectors).forEach(([tab, selector]) => {
|
||||
Object.entries(tabSelectors).forEach(([tab, selector]) => {
|
||||
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;
|
||||
const target = sidebar.querySelector(selector);
|
||||
if (target) {
|
||||
const tabBtn = target.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || target;
|
||||
if (shouldHide) {
|
||||
if (tabBtn.style.display !== "none") tabBtn.style.setProperty("display", "none", "important");
|
||||
} else {
|
||||
@@ -186,9 +187,9 @@ function updateSidebarTabsVisibility() {
|
||||
});
|
||||
|
||||
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;
|
||||
const historyTarget = sidebar.querySelector('[data-testid="queue-tab-button"], [data-testid="job-history-tab-button"], [class*="lucide--history"]');
|
||||
if (historyTarget) {
|
||||
const historyBtn = historyTarget.closest('.comfyui-sidebar-tab, button, [role="tab"], li, a, .comfyui-sidebar-item') || historyTarget;
|
||||
if (hideStockHistory) {
|
||||
if (historyBtn.style.display !== "none") historyBtn.style.setProperty("display", "none", "important");
|
||||
} else {
|
||||
@@ -223,141 +224,148 @@ function cleanHTML(html) {
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
function syncGraphButton() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncExtensionsPanel(isClassicLayoutEnabled) {
|
||||
const extensionsPanel = findNativeExtensionsPanel();
|
||||
if (!extensionsPanel) return;
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
function syncPropertiesButton(isClassicLayoutEnabled) {
|
||||
if (!isClassicLayoutEnabled) {
|
||||
const originalBtn = findOriginalPropertiesButton();
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
const originalBtn = findOriginalPropertiesButton();
|
||||
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 = findOriginalPropertiesButton();
|
||||
if (nativeBtn) {
|
||||
nativeBtn.click();
|
||||
requestAnimationFrame(() => syncClassicLayout());
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(customBtn);
|
||||
}
|
||||
|
||||
if (customBtn) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
function syncActiveQueueIndicator(isClassicLayoutEnabled) {
|
||||
const hideQueueIndicator = app.ui?.settings?.getSettingValue("Comfy Sidebar.Hide Junk.Override Stock Job History Tab") ?? false;
|
||||
const indicator = findActiveQueueIndicator();
|
||||
if (indicator) {
|
||||
if (isClassicLayoutEnabled && hideQueueIndicator) {
|
||||
if (indicator.style.display !== "none") indicator.style.setProperty("display", "none", "important");
|
||||
} else if (indicator.style.display === "none") {
|
||||
indicator.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function syncClassicLayout() {
|
||||
if (domObserver) domObserver.disconnect();
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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 = findOriginalPropertiesButton();
|
||||
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 = findOriginalPropertiesButton();
|
||||
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 = findOriginalPropertiesButton();
|
||||
if (nativeBtn) {
|
||||
nativeBtn.click();
|
||||
requestAnimationFrame(() => syncClassicLayout());
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(customBtn);
|
||||
}
|
||||
|
||||
if (customBtn) {
|
||||
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) {
|
||||
if (hideQueueIndicator) {
|
||||
if (indicator.style.display !== "none") indicator.style.setProperty("display", "none", "important");
|
||||
} else if (indicator.style.display === "none") {
|
||||
indicator.style.removeProperty("display");
|
||||
}
|
||||
}
|
||||
|
||||
syncGraphButton();
|
||||
syncExtensionsPanel(isClassicLayoutEnabled);
|
||||
syncPropertiesButton(isClassicLayoutEnabled);
|
||||
syncActiveQueueIndicator(isClassicLayoutEnabled);
|
||||
updateSidebarTabsVisibility();
|
||||
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app } from "/scripts/app.js";
|
||||
import { SettingIds } from "./core/constants.js";
|
||||
import { injectStyles } from "./utils/styles.js";
|
||||
import { setupDragAndDrop } from "./utils/dragdrop.js";
|
||||
import { renumberNodesTopologically, setManualNodeId } from "./utils/nodeRenumber.js";
|
||||
import { findOurSidebarButton } from "./comfy/adapter.js";
|
||||
import { syncAllNodeBadges, setupVueNodeObserver, toggleIgnoreActiveNode } from "./comfy/nodes.js";
|
||||
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings, destroyLayoutFix } from "./comfy/layout.js";
|
||||
@@ -115,6 +116,13 @@ function setupKeydownShortcuts() {
|
||||
e.stopPropagation();
|
||||
toggleIgnoreActiveNode(() => renderSidebar());
|
||||
}
|
||||
|
||||
// Alt+R: Renumber all nodes in execution order
|
||||
if (e.key.toLowerCase() === "r" && e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
renumberNodesTopologically();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", activeKeydownHandler, true);
|
||||
@@ -140,9 +148,39 @@ app.registerExtension({
|
||||
id: "ComfySidebar.ToggleIgnoreNode",
|
||||
label: "Toggle Ignore Selected Node(s) in Queue",
|
||||
function: () => toggleIgnoreActiveNode(() => renderSidebar())
|
||||
},
|
||||
{
|
||||
id: "ComfySidebar.RenumberNodes",
|
||||
label: "Renumber All Nodes (Execution Order)",
|
||||
function: () => renumberNodesTopologically()
|
||||
},
|
||||
{
|
||||
id: "ComfySidebar.SetNodeId",
|
||||
label: "Set Selected Node ID...",
|
||||
function: () => setManualNodeId()
|
||||
}
|
||||
],
|
||||
|
||||
getNodeMenuItems(node) {
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "Set Node ID...",
|
||||
callback: () => setManualNodeId(node)
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
getCanvasMenuItems() {
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "Renumber All Nodes (Execution Order)",
|
||||
callback: () => renumberNodesTopologically()
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
nodeCreated(node) {
|
||||
if (node && node.properties?.ignoreInQueue) {
|
||||
node.boxcolor = "#ff3333";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isAudioViewerOpen as checkAudioViewerOpen } from "../utils/comparison.js";
|
||||
|
||||
let currentlyPlayingAudio = null;
|
||||
let activeComparisonViewer = null;
|
||||
|
||||
export function stopAllAudioPlayback() {
|
||||
if (currentlyPlayingAudio) {
|
||||
@@ -21,14 +22,6 @@ export function getCurrentlyPlayingAudio() {
|
||||
return currentlyPlayingAudio;
|
||||
}
|
||||
|
||||
export function setActiveComparisonViewer(viewer) {
|
||||
activeComparisonViewer = viewer;
|
||||
}
|
||||
|
||||
export function getActiveComparisonViewer() {
|
||||
return activeComparisonViewer;
|
||||
}
|
||||
|
||||
export function isAudioViewerOpen() {
|
||||
return !!(activeComparisonViewer && activeComparisonViewer.isAudio);
|
||||
return checkAudioViewerOpen();
|
||||
}
|
||||
|
||||
+63
-3
@@ -2,7 +2,7 @@ import { app } from "/scripts/app.js";
|
||||
import { PromptStatus } from "../core/constants.js";
|
||||
import { store } from "../core/store.js";
|
||||
import { renderCardImages } from "./mediaPreview.js";
|
||||
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage } from "./mediaActions.js";
|
||||
import { copyImageToClipboard, openFileOrFolder, deleteFileOnServer, findNodeIdForImage, removeImageFromNodeOutputs } from "./mediaActions.js";
|
||||
import { centerAndSelectCanvasNode } from "../comfy/adapter.js";
|
||||
import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/utils.js";
|
||||
import { showFullscreenPreview } from "../utils/comparison.js";
|
||||
@@ -158,17 +158,75 @@ export function syncCardButtonVisibility(cardObj, state) {
|
||||
(state.status !== PromptStatus.COMPLETED && validOutputs.length > 0);
|
||||
|
||||
if (hasIntermediates) {
|
||||
// Check if intermediate files still exist on disk
|
||||
for (const out of distinctOutputs) {
|
||||
for (const img of out.images) {
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
if (!src.startsWith("blob:")) {
|
||||
fetch(src, { method: "HEAD" }).then(res => {
|
||||
if (res.status === 404) {
|
||||
const liveState = store.getPrompt(state.pid);
|
||||
if (liveState && liveState.nodeOutputs) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
syncCardButtonVisibility(cardObj, liveState);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cardObj.leftHoverBtn.style.removeProperty("display");
|
||||
cardObj.leftHoverBtn.style.display = "inline-flex";
|
||||
cardObj.leftHoverBtn.onclick = (ev) => {
|
||||
|
||||
cardObj.leftHoverBtn.onclick = async (ev) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
// On click: verify distinct outputs still exist before opening
|
||||
const liveState = store.getPrompt(state.pid) || state;
|
||||
const curOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow);
|
||||
const primSigs = new Set((liveState.images || []).map(i => `${i.subfolder || ""}/${i.filename}`));
|
||||
const curDistinct = curOutputs.filter(o => o.images && o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
|
||||
|
||||
if (curDistinct.length === 0) {
|
||||
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
||||
return;
|
||||
}
|
||||
|
||||
let anyMissing = false;
|
||||
for (const out of curDistinct) {
|
||||
for (const img of out.images) {
|
||||
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
if (!src.startsWith("blob:")) {
|
||||
try {
|
||||
const res = await fetch(src, { method: "HEAD" });
|
||||
if (res.status === 404) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
anyMissing = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyMissing) {
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
syncCardButtonVisibility(cardObj, liveState);
|
||||
const remaining = getRunOutputs(liveState.nodeOutputs, liveState.workflow)
|
||||
.filter(o => o.images && o.images.some(img => !primSigs.has(`${img.subfolder || ""}/${img.filename}`)));
|
||||
if (remaining.length === 0) {
|
||||
return; // All intermediate outputs were wiped; button is now hidden
|
||||
}
|
||||
}
|
||||
|
||||
const scrollEl = document.querySelector('.sidebar-content-container, [class*="sidebar-content-container"], [class*="overflow-y-auto"]')
|
||||
|| store.ui.cardStack?.parentElement
|
||||
|| store.ui.cardStack;
|
||||
if (scrollEl) {
|
||||
store.ui.mainQueueScrollTop = scrollEl.scrollTop;
|
||||
}
|
||||
store.openOutputsSubmenu(state.pid);
|
||||
store.openOutputsSubmenu(liveState.pid);
|
||||
};
|
||||
} else {
|
||||
cardObj.leftHoverBtn.style.setProperty("display", "none", "important");
|
||||
@@ -451,12 +509,14 @@ export function updateCardDOM(cardObj, state, showPendingSummary = true, showWor
|
||||
|
||||
if (state.status === PromptStatus.PENDING && !showPendingSummary) {
|
||||
cardObj.cancelBtn.style.display = "flex";
|
||||
cardObj.cancelBtn.title = "Cancel Queued Task";
|
||||
cardObj.cancelBtn.onclick = async (ev) => {
|
||||
ev.stopPropagation();
|
||||
cancelPendingTask(state.pid);
|
||||
};
|
||||
} else if (state.status === PromptStatus.ACTIVE) {
|
||||
cardObj.cancelBtn.style.display = "flex";
|
||||
cardObj.cancelBtn.title = "Interrupt Execution";
|
||||
cardObj.cancelBtn.onclick = async (ev) => {
|
||||
ev.stopPropagation();
|
||||
interruptActive();
|
||||
|
||||
+84
-11
@@ -1,8 +1,9 @@
|
||||
import { isImageFormat, isVideoFormat, is3DFormat, isAudioFormat, getFilenameFromUrl } from "../utils/utils.js";
|
||||
import { openFileOrFolder } from "./mediaActions.js";
|
||||
import { openFileOrFolder, removeImageFromNodeOutputs } from "./mediaActions.js";
|
||||
import { stopAllAudioPlayback, isAudioViewerOpen, setCurrentlyPlayingAudio, getCurrentlyPlayingAudio } from "./audioController.js";
|
||||
import { showFullscreenPreview } from "../utils/comparison.js";
|
||||
import { store } from "../core/store.js";
|
||||
import { PromptStatus } from "../core/constants.js";
|
||||
|
||||
export function render3DCardPreview(cardObj, wrapper, src, img, state) {
|
||||
let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper");
|
||||
@@ -272,6 +273,9 @@ export function renderAudioCardPreview(cardObj, wrapper, src, img, state) {
|
||||
}
|
||||
|
||||
export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state) {
|
||||
if (cardObj.dimEl) cardObj.dimEl.style.display = "none";
|
||||
const playIcon = wrapper.querySelector(".comfy-sidebar-play-icon");
|
||||
if (playIcon) playIcon.remove();
|
||||
let previewFile = wrapper.querySelector(".comfy-sidebar-file-wrapper");
|
||||
const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`;
|
||||
const filename = img.filename || getFilenameFromUrl(src) || "output_file";
|
||||
@@ -441,18 +445,87 @@ export function renderCardImages(cardObj, state, onNavigateBatch) {
|
||||
showFullscreenPreview([activeSrc], ev.shiftKey, state?.pid);
|
||||
};
|
||||
|
||||
mediaEl.onerror = async () => {
|
||||
if (src && !src.startsWith("blob:") && !is3D && !isAudio) {
|
||||
try {
|
||||
const res = await fetch(src, { method: "HEAD" });
|
||||
if (res.status === 404) {
|
||||
store.deletePrompt(state.pid);
|
||||
if (cardObj.element) cardObj.element.remove();
|
||||
return;
|
||||
mediaEl.onerror = () => {
|
||||
mediaEl.onerror = null;
|
||||
if ((src && src.startsWith("blob:")) || is3D || isAudio) return;
|
||||
|
||||
const cardId = cardObj.element?.id || "";
|
||||
|
||||
// 1. If an intermediate output item is missing, remove only this item
|
||||
if (cardId.startsWith("card-submenu-")) {
|
||||
const parentState = store.getPrompt(state.pid);
|
||||
if (parentState && parentState.nodeOutputs) {
|
||||
if (state.nodeId && parentState.nodeOutputs[state.nodeId]) {
|
||||
delete parentState.nodeOutputs[state.nodeId];
|
||||
} else {
|
||||
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
|
||||
}
|
||||
} catch (e) {}
|
||||
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
|
||||
store.updatePrompt(parentState.pid, parentState);
|
||||
}
|
||||
cardObj.element?.remove();
|
||||
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
|
||||
if (!remaining || remaining.length === 0) {
|
||||
store.closeSubmenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If a batch item is missing, remove only this item from the batch
|
||||
if (cardId.startsWith("card-batch-")) {
|
||||
const parentPid = state.parentPromptId || state.pid;
|
||||
const parentState = store.getPrompt(parentPid);
|
||||
if (parentState) {
|
||||
if (Array.isArray(parentState.images)) {
|
||||
const idx = parentState.images.findIndex(i => i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""));
|
||||
if (idx > -1) parentState.images.splice(idx, 1);
|
||||
}
|
||||
if (parentState.nodeOutputs) {
|
||||
removeImageFromNodeOutputs(parentState.nodeOutputs, img);
|
||||
}
|
||||
store.updatePrompt(parentPid, parentState);
|
||||
}
|
||||
cardObj.element?.remove();
|
||||
const remaining = store.ui.cardStack?.querySelectorAll('.comfy-sidebar-card');
|
||||
if (!remaining || remaining.length === 0) {
|
||||
store.closeSubmenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Main card: if this file is gone, clean it up
|
||||
const liveState = store.getPrompt(state.pid);
|
||||
if (liveState) {
|
||||
if (Array.isArray(liveState.images)) {
|
||||
liveState.images = liveState.images.filter(i =>
|
||||
!(i.filename === img.filename && (i.subfolder || "") === (img.subfolder || ""))
|
||||
);
|
||||
}
|
||||
if (liveState.nodeOutputs) {
|
||||
removeImageFromNodeOutputs(liveState.nodeOutputs, img);
|
||||
}
|
||||
|
||||
// If the card still has other images (e.g. from a batch), persist and switch to next image
|
||||
if (liveState.images && liveState.images.length > 0) {
|
||||
cardObj.currentImageIndex = 0;
|
||||
cardObj.lastImagesSignature = "";
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the card has text outputs or is still in progress, persist
|
||||
if ((liveState.texts && liveState.texts.length > 0) || (liveState.status && liveState.status !== PromptStatus.COMPLETED)) {
|
||||
cardObj.lastImagesSignature = "";
|
||||
store.updatePrompt(liveState.pid, liveState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only delete the main card when all items belonging to it are gone
|
||||
store.deletePrompt(liveState.pid);
|
||||
cardObj.element?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
renderGenericFileCardPreview(cardObj, wrapper, src, img, state);
|
||||
};
|
||||
|
||||
const applyDimensions = (width, height) => {
|
||||
|
||||
+152
-37
@@ -7,11 +7,87 @@ import { updateSidebarBadge, centerAndSelectCanvasNode } from "../comfy/adapter.
|
||||
import { matchesFilter, getRunOutputs, isImageFormat } from "../utils/utils.js";
|
||||
import { renderCardImages } from "./mediaPreview.js";
|
||||
import { deleteFileOnServer, removeImageFromNodeOutputs, copyImageToClipboard, findNodeIdForImage } from "./mediaActions.js";
|
||||
import { toggleIgnoreActiveNode } from "../comfy/nodes.js";
|
||||
import { renumberNodesTopologically, showNotification } from "../utils/nodeRenumber.js";
|
||||
|
||||
let scrollToTopBtnEl = null;
|
||||
let globalClickRegistered = false;
|
||||
let renderAnimationFrameId = null;
|
||||
let activeTimerInterval = null;
|
||||
let currentColumnCount = 1;
|
||||
|
||||
function mountCards(cardStack, fullWidthEls, cardEls, cols = 1) {
|
||||
if (!cardStack) return;
|
||||
|
||||
// 1. Full-width container (for banners like Pending Queue summary)
|
||||
let bannerContainer = cardStack.querySelector('.comfy-sidebar-banners');
|
||||
if (!bannerContainer) {
|
||||
bannerContainer = document.createElement('div');
|
||||
bannerContainer.className = 'comfy-sidebar-banners';
|
||||
Object.assign(bannerContainer.style, { display: 'flex', flexDirection: 'column', gap: '12px', width: '100%' });
|
||||
cardStack.insertBefore(bannerContainer, cardStack.firstChild);
|
||||
}
|
||||
fullWidthEls.forEach((el, i) => {
|
||||
if (bannerContainer.children[i] !== el) {
|
||||
bannerContainer.insertBefore(el, bannerContainer.children[i] || null);
|
||||
}
|
||||
});
|
||||
while (bannerContainer.children.length > fullWidthEls.length) {
|
||||
bannerContainer.removeChild(bannerContainer.lastChild);
|
||||
}
|
||||
bannerContainer.style.display = fullWidthEls.length > 0 ? 'flex' : 'none';
|
||||
bannerContainer.style.marginBottom = fullWidthEls.length > 0 ? '12px' : '0';
|
||||
|
||||
// 2. Masonry Columns container
|
||||
let columnsWrapper = cardStack.querySelector('.comfy-sidebar-masonry-wrapper');
|
||||
if (!columnsWrapper) {
|
||||
columnsWrapper = document.createElement('div');
|
||||
columnsWrapper.className = 'comfy-sidebar-masonry-wrapper';
|
||||
Object.assign(columnsWrapper.style, { display: 'flex', gap: '12px', alignItems: 'flex-start', width: '100%' });
|
||||
cardStack.appendChild(columnsWrapper);
|
||||
}
|
||||
|
||||
while (columnsWrapper.children.length < cols) {
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'comfy-sidebar-masonry-col';
|
||||
Object.assign(colDiv.style, { flex: '1', display: 'flex', flexDirection: 'column', gap: '12px', minWidth: '0' });
|
||||
columnsWrapper.appendChild(colDiv);
|
||||
}
|
||||
while (columnsWrapper.children.length > cols) {
|
||||
columnsWrapper.removeChild(columnsWrapper.lastChild);
|
||||
}
|
||||
|
||||
// Distribute cards into the shortest column so short cards stack next to tall cards
|
||||
const colBuckets = Array.from({ length: cols }, () => []);
|
||||
const colHeights = new Array(cols).fill(0);
|
||||
|
||||
cardEls.forEach((cardEl) => {
|
||||
let shortestCol = 0;
|
||||
for (let c = 1; c < cols; c++) {
|
||||
if (colHeights[c] < colHeights[shortestCol]) {
|
||||
shortestCol = c;
|
||||
}
|
||||
}
|
||||
colBuckets[shortestCol].push(cardEl);
|
||||
|
||||
const cardH = cardEl.offsetHeight > 0 ? cardEl.offsetHeight : (cardEl._cachedHeight || 160);
|
||||
cardEl._cachedHeight = cardH;
|
||||
colHeights[shortestCol] += cardH + 12;
|
||||
});
|
||||
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const colDiv = columnsWrapper.children[c];
|
||||
const bucket = colBuckets[c];
|
||||
bucket.forEach((el, i) => {
|
||||
if (colDiv.children[i] !== el) {
|
||||
colDiv.insertBefore(el, colDiv.children[i] || null);
|
||||
}
|
||||
});
|
||||
while (colDiv.children.length > bucket.length) {
|
||||
colDiv.removeChild(colDiv.lastChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollContainer() {
|
||||
if (!store.ui.cardStack) return null;
|
||||
@@ -112,7 +188,20 @@ export function setupSidebarView() {
|
||||
standardHeader.appendChild(titleGroup);
|
||||
|
||||
const actionsGroup = document.createElement("div");
|
||||
Object.assign(actionsGroup.style, { display: "flex", gap: "6px", alignItems: "center" });
|
||||
Object.assign(actionsGroup.style, { display: "flex", gap: "5px", alignItems: "center" });
|
||||
|
||||
const createInstantBtn = (iconClass, tooltip, onClickFn) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `${iconClass} comfy-sidebar-header-btn`;
|
||||
btn.title = tooltip;
|
||||
btn.addEventListener("click", async (ev) => {
|
||||
ev.stopPropagation();
|
||||
btn.style.transform = "scale(0.88)";
|
||||
setTimeout(() => { btn.style.transform = ""; }, 120);
|
||||
await onClickFn();
|
||||
});
|
||||
return btn;
|
||||
};
|
||||
|
||||
const createActionBtn = (iconClass, tooltip, hoverColor, onClickFn) => {
|
||||
const btn = document.createElement("button");
|
||||
@@ -146,6 +235,31 @@ export function setupSidebarView() {
|
||||
return btn;
|
||||
};
|
||||
|
||||
const btnToggleIgnore = createInstantBtn("pi pi-eye-slash", "Toggle Ignore Selected Node(s) in Queue (Ctrl+Q)", () => {
|
||||
const raw = app.canvas?.selected_nodes;
|
||||
const selected = raw instanceof Map || raw instanceof Set
|
||||
? Array.from(raw.values())
|
||||
: (Array.isArray(raw) ? raw : (raw && typeof raw === "object" ? Object.values(raw) : []));
|
||||
const selectedNodes = selected.length > 0 ? selected : (app.canvas?.selected_node ? [app.canvas.selected_node] : []);
|
||||
|
||||
if (selectedNodes.length === 0) {
|
||||
showNotification("No nodes selected. Click a node on the canvas first to toggle ignore.", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
toggleIgnoreActiveNode(() => renderSidebar());
|
||||
});
|
||||
|
||||
const btnRenumber = createInstantBtn("pi pi-sort-numeric-down", "Renumber All Nodes by Execution Order (Alt+R)", async () => {
|
||||
await renumberNodesTopologically();
|
||||
});
|
||||
|
||||
const divider = document.createElement("div");
|
||||
Object.assign(divider.style, {
|
||||
width: "1px", height: "14px", background: "var(--border-color, #444)",
|
||||
margin: "0 2px", opacity: "0.6"
|
||||
});
|
||||
|
||||
const btnClearInterrupted = createActionBtn("pi pi-eraser", "Clear Cancelled & Failed", "#ffc107", async () => {
|
||||
await clearCancelledOrFailed();
|
||||
renderSidebar();
|
||||
@@ -156,6 +270,9 @@ export function setupSidebarView() {
|
||||
renderSidebar();
|
||||
});
|
||||
|
||||
actionsGroup.appendChild(btnToggleIgnore);
|
||||
actionsGroup.appendChild(btnRenumber);
|
||||
actionsGroup.appendChild(divider);
|
||||
actionsGroup.appendChild(btnClearInterrupted);
|
||||
actionsGroup.appendChild(btnClearAll);
|
||||
standardHeader.appendChild(actionsGroup);
|
||||
@@ -213,7 +330,13 @@ export function setupSidebarView() {
|
||||
|
||||
const cardStack = document.createElement("div");
|
||||
store.ui.cardStack = cardStack;
|
||||
Object.assign(cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
|
||||
Object.assign(cardStack.style, {
|
||||
flex: "1",
|
||||
overflowY: "visible",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
paddingBottom: "28px"
|
||||
});
|
||||
container.appendChild(cardStack);
|
||||
|
||||
scrollToTopBtnEl = document.createElement("button");
|
||||
@@ -233,9 +356,9 @@ export function setupSidebarView() {
|
||||
new ResizeObserver((entries) => {
|
||||
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
|
||||
const cols = Math.max(1, Math.floor(entries[0].contentRect.width / (threshold / 2)));
|
||||
if (store.ui.cardStack) {
|
||||
store.ui.cardStack.style.columnCount = cols.toString();
|
||||
store.ui.cardStack.style.columnGap = cols > 1 ? "12px" : "0";
|
||||
if (cols !== currentColumnCount) {
|
||||
currentColumnCount = cols;
|
||||
renderSidebar();
|
||||
}
|
||||
}).observe(container);
|
||||
|
||||
@@ -504,14 +627,7 @@ export function renderSidebar() {
|
||||
targetElements.push(cardObj.element);
|
||||
});
|
||||
|
||||
targetElements.forEach((el, index) => {
|
||||
if (store.ui.cardStack.children[index] !== el) {
|
||||
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
|
||||
}
|
||||
});
|
||||
while (store.ui.cardStack.children.length > targetElements.length) {
|
||||
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
|
||||
}
|
||||
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
|
||||
|
||||
const scrollEl = getScrollContainer();
|
||||
if (scrollEl) scrollEl.scrollTop = 0;
|
||||
@@ -568,7 +684,8 @@ export function renderSidebar() {
|
||||
const emptyCard = document.createElement("div");
|
||||
emptyCard.className = "comfy-sidebar-card completed";
|
||||
Object.assign(emptyCard.style, {
|
||||
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px"
|
||||
padding: "16px", textAlign: "center", color: "var(--desc-color, #aaa)", fontSize: "12px",
|
||||
gridColumn: "1 / -1"
|
||||
});
|
||||
emptyCard.textContent = "No separate intermediate outputs found.";
|
||||
targetElements.push(emptyCard);
|
||||
@@ -749,6 +866,7 @@ export function renderSidebar() {
|
||||
cardObj.placeholder.style.display = "none";
|
||||
renderCardImages(cardObj, {
|
||||
pid: st.pid,
|
||||
nodeId: out.nodeId,
|
||||
status: PromptStatus.COMPLETED,
|
||||
images: out.images,
|
||||
workflow: st.workflow,
|
||||
@@ -777,13 +895,10 @@ export function renderSidebar() {
|
||||
});
|
||||
}
|
||||
|
||||
targetElements.forEach((el, index) => {
|
||||
if (store.ui.cardStack.children[index] !== el) {
|
||||
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
|
||||
}
|
||||
});
|
||||
while (store.ui.cardStack.children.length > targetElements.length) {
|
||||
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
|
||||
if (outputs.length === 0) {
|
||||
mountCards(store.ui.cardStack, targetElements, [], currentColumnCount);
|
||||
} else {
|
||||
mountCards(store.ui.cardStack, [], targetElements, currentColumnCount);
|
||||
}
|
||||
|
||||
updateScrollTopBtnVisibility();
|
||||
@@ -817,7 +932,8 @@ export function renderSidebar() {
|
||||
return updateCardDOM(cardObj, state, showPendingSummary, showWorkingNode);
|
||||
};
|
||||
|
||||
const targetElements = [];
|
||||
const bannerElements = [];
|
||||
const cardElements = [];
|
||||
const pendingCount = store.getAllPrompts().filter(t => t.status === PromptStatus.PENDING).length;
|
||||
|
||||
if (pendingCount > 0) {
|
||||
@@ -827,8 +943,8 @@ export function renderSidebar() {
|
||||
const el = document.createElement("div");
|
||||
Object.assign(el.style, {
|
||||
background: "var(--comfy-input-bg, #181818)", border: "2px solid #6c757d", borderRadius: "4px", padding: "10px",
|
||||
marginBottom: "12px", textAlign: "center", fontSize: "12px", fontWeight: "bold",
|
||||
color: "var(--desc-color, #aaa)", breakInside: "avoid", display: "flex", flexDirection: "column", gap: "8px"
|
||||
textAlign: "center", fontSize: "12px", fontWeight: "bold",
|
||||
color: "var(--desc-color, #aaa)", display: "flex", flexDirection: "column", gap: "8px"
|
||||
});
|
||||
const textDiv = document.createElement("div");
|
||||
el.appendChild(textDiv);
|
||||
@@ -846,7 +962,7 @@ export function renderSidebar() {
|
||||
cardPool.set("pending-summary-card", pCard);
|
||||
}
|
||||
pCard.textDiv.textContent = `Pending Queue: ${pendingCount} tasks`;
|
||||
targetElements.push(pCard.element);
|
||||
bannerElements.push(pCard.element);
|
||||
} else {
|
||||
let btnCard = cardPool.get("pending-cancel-all-standalone");
|
||||
if (!btnCard) {
|
||||
@@ -854,7 +970,7 @@ export function renderSidebar() {
|
||||
btn.textContent = "Cancel All Pending";
|
||||
Object.assign(btn.style, {
|
||||
background: "#dc3545", color: "white", border: "none", borderRadius: "3px", padding: "6px",
|
||||
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%", marginBottom: "12px", breakInside: "avoid"
|
||||
cursor: "pointer", fontSize: "11px", fontWeight: "bold", width: "100%"
|
||||
});
|
||||
btn.onclick = async () => {
|
||||
await cancelAllPending();
|
||||
@@ -862,23 +978,22 @@ export function renderSidebar() {
|
||||
btnCard = { element: btn };
|
||||
cardPool.set("pending-cancel-all-standalone", btnCard);
|
||||
}
|
||||
targetElements.push(btnCard.element);
|
||||
pendingTasks.forEach(st => targetElements.push(syncTaskCard(st)));
|
||||
bannerElements.push(btnCard.element);
|
||||
pendingTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
||||
}
|
||||
}
|
||||
|
||||
activeTasks.forEach(st => targetElements.push(syncTaskCard(st)));
|
||||
completedTasks.forEach(st => targetElements.push(syncTaskCard(st)));
|
||||
activeTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
||||
completedTasks.forEach(st => cardElements.push(syncTaskCard(st)));
|
||||
|
||||
targetElements.forEach((el, index) => {
|
||||
if (store.ui.cardStack.children[index] !== el) {
|
||||
store.ui.cardStack.insertBefore(el, store.ui.cardStack.children[index] || null);
|
||||
}
|
||||
});
|
||||
while (store.ui.cardStack.children.length > targetElements.length) {
|
||||
store.ui.cardStack.removeChild(store.ui.cardStack.lastChild);
|
||||
const threshold = app.ui?.settings?.getSettingValue?.(SettingIds.GRID_COLUMNS_THRESHOLD) ?? 350;
|
||||
const containerWidth = store.ui.sidebarContainer?.clientWidth || 0;
|
||||
if (containerWidth > 0) {
|
||||
currentColumnCount = Math.max(1, Math.floor(containerWidth / (threshold / 2)));
|
||||
}
|
||||
|
||||
mountCards(store.ui.cardStack, bannerElements, cardElements, currentColumnCount);
|
||||
|
||||
if (store.ui.mainQueueScrollTop !== null) {
|
||||
const restorePos = store.ui.mainQueueScrollTop;
|
||||
store.ui.mainQueueScrollTop = null;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { syncAllNodeBadges } from "../comfy/nodes.js";
|
||||
|
||||
export function showNotification(message, type = "success") {
|
||||
const summary = type === "error" ? "Error" : type === "warn" ? "Warning" : "Node Renumber";
|
||||
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: type === "error" ? "error" : type === "warn" ? "warn" : "success",
|
||||
summary,
|
||||
detail: message,
|
||||
life: 4000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Elegant non-blocking fallback if native toast is unavailable
|
||||
const id = "comfy-sidebar-toast";
|
||||
let toast = document.getElementById(id);
|
||||
if (!toast) {
|
||||
toast = document.createElement("div");
|
||||
toast.id = id;
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
background: #18181b;
|
||||
color: #f4f4f5;
|
||||
padding: 12px 18px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid ${type === "error" ? "#ef4444" : type === "warn" ? "#f59e0b" : "#10b981"};
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
z-index: 99999;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
max-width: 380px;
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
pointer-events: none;
|
||||
`;
|
||||
toast.innerHTML = `<strong style="color:${type === "error" ? "#f87171" : type === "warn" ? "#fbbf24" : "#34d399"}">${summary}</strong><div style="margin-top:2px;">${message}</div>`;
|
||||
requestAnimationFrame(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateY(0)";
|
||||
});
|
||||
clearTimeout(toast._timeout);
|
||||
toast._timeout = setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateY(10px)";
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
export async function renumberNodesTopologically() {
|
||||
if (!app.graph || !app.graph._nodes || app.graph._nodes.length === 0) {
|
||||
showNotification("No nodes found on canvas to renumber.", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Compute LiteGraph's topological execution order
|
||||
if (typeof app.graph.computeExecutionOrder === "function") {
|
||||
app.graph.computeExecutionOrder(false);
|
||||
}
|
||||
|
||||
// Sort nodes by order (lowest executes first; highest executes last)
|
||||
const sortedNodes = [...app.graph._nodes].sort((a, b) => {
|
||||
const orderA = typeof a.order === "number" ? a.order : Infinity;
|
||||
const orderB = typeof b.order === "number" ? b.order : Infinity;
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
|
||||
const posXA = Array.isArray(a.pos) ? a.pos[0] : 0;
|
||||
const posXB = Array.isArray(b.pos) ? b.pos[0] : 0;
|
||||
if (posXA !== posXB) return posXA - posXB;
|
||||
|
||||
const posYA = Array.isArray(a.pos) ? a.pos[1] : 0;
|
||||
const posYB = Array.isArray(b.pos) ? b.pos[1] : 0;
|
||||
return posYA - posYB;
|
||||
});
|
||||
|
||||
const oldToNew = new Map();
|
||||
sortedNodes.forEach((node, index) => {
|
||||
const newId = index + 1;
|
||||
oldToNew.set(node.id, newId);
|
||||
oldToNew.set(String(node.id), newId);
|
||||
});
|
||||
|
||||
// 1. Remap node IDs in-place on app.graph
|
||||
app.graph._nodes_by_id = {};
|
||||
sortedNodes.forEach(node => {
|
||||
const oldId = node.id;
|
||||
const newId = oldToNew.get(oldId);
|
||||
if (newId != null) {
|
||||
// Update internal widget IDs like "$2034-0" (PreviewBridge)
|
||||
if (Array.isArray(node.widgets)) {
|
||||
for (const w of node.widgets) {
|
||||
if (typeof w.value === "string" && w.value.startsWith(`$${oldId}-`)) {
|
||||
w.value = w.value.replace(`$${oldId}-`, `$${newId}-`);
|
||||
}
|
||||
}
|
||||
}
|
||||
node.id = newId;
|
||||
app.graph._nodes_by_id[newId] = node;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Remap links
|
||||
if (app.graph.links) {
|
||||
const links = app.graph.links instanceof Map
|
||||
? Array.from(app.graph.links.values())
|
||||
: Object.values(app.graph.links);
|
||||
|
||||
links.forEach(link => {
|
||||
if (!link) return;
|
||||
if (oldToNew.has(link.origin_id)) link.origin_id = oldToNew.get(link.origin_id);
|
||||
if (oldToNew.has(link.target_id)) link.target_id = oldToNew.get(link.target_id);
|
||||
});
|
||||
}
|
||||
|
||||
app.graph.last_node_id = sortedNodes.length;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
if (app.canvas?.draw) {
|
||||
app.canvas.draw(true, true);
|
||||
}
|
||||
syncAllNodeBadges();
|
||||
|
||||
const lastNode = sortedNodes[sortedNodes.length - 1];
|
||||
const lastNodeName = lastNode ? (lastNode.title || lastNode.type) : "SaveImage";
|
||||
showNotification(`Renumbered ${sortedNodes.length} nodes by execution order. Latest node: #${lastNode.id} (${lastNodeName}).`, "success");
|
||||
} catch (err) {
|
||||
console.error("ComfySidebar: Failed to renumber nodes", err);
|
||||
showNotification("Failed to renumber nodes: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
export async function setManualNodeId(targetNode = null) {
|
||||
if (!app.graph) return;
|
||||
const node = targetNode || (app.canvas?.selected_nodes ? Object.values(app.canvas.selected_nodes)[0] : null);
|
||||
if (!node) {
|
||||
showNotification("Please select a node first to change its ID.", "warn");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentId = node.id;
|
||||
const input = prompt(`Enter new ID for node #${currentId} (${node.title || node.type}):`, String(currentId));
|
||||
if (!input) return;
|
||||
|
||||
const newId = parseInt(input.trim(), 10);
|
||||
if (isNaN(newId) || newId <= 0) {
|
||||
showNotification("Please enter a valid positive number.", "error");
|
||||
return;
|
||||
}
|
||||
if (newId === currentId) return;
|
||||
|
||||
try {
|
||||
const existingNode = app.graph.getNodeById ? app.graph.getNodeById(newId) : null;
|
||||
if (existingNode && existingNode !== node) {
|
||||
const confirmSwap = confirm(`Node ID #${newId} is already used by "${existingNode.title || existingNode.type}". Swap their IDs?`);
|
||||
if (!confirmSwap) return;
|
||||
}
|
||||
|
||||
const oldToNew = new Map();
|
||||
if (existingNode && existingNode !== node) {
|
||||
oldToNew.set(currentId, newId);
|
||||
oldToNew.set(newId, currentId);
|
||||
existingNode.id = currentId;
|
||||
} else {
|
||||
oldToNew.set(currentId, newId);
|
||||
}
|
||||
node.id = newId;
|
||||
|
||||
// Rebuild lookup map
|
||||
app.graph._nodes_by_id = {};
|
||||
for (const n of app.graph._nodes) {
|
||||
app.graph._nodes_by_id[n.id] = n;
|
||||
}
|
||||
|
||||
// Remap links
|
||||
if (app.graph.links) {
|
||||
const links = app.graph.links instanceof Map
|
||||
? Array.from(app.graph.links.values())
|
||||
: Object.values(app.graph.links);
|
||||
|
||||
links.forEach(link => {
|
||||
if (!link) return;
|
||||
if (oldToNew.has(link.origin_id)) link.origin_id = oldToNew.get(link.origin_id);
|
||||
if (oldToNew.has(link.target_id)) link.target_id = oldToNew.get(link.target_id);
|
||||
});
|
||||
}
|
||||
|
||||
app.graph.last_node_id = Math.max(app.graph.last_node_id || 0, newId, app.graph._nodes.length);
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
if (app.canvas?.draw) {
|
||||
app.canvas.draw(true, true);
|
||||
}
|
||||
syncAllNodeBadges();
|
||||
showNotification(`Assigned node ID #${newId} to "${node.title || node.type}".`, "success");
|
||||
} catch (err) {
|
||||
console.error("ComfySidebar: Failed to set node ID", err);
|
||||
showNotification("Failed to set node ID: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ export function injectStyles() {
|
||||
.comfy-sidebar-card {
|
||||
background: var(--comfy-input-bg, #181818);
|
||||
border-radius: 4px; padding: 8px; position: relative;
|
||||
min-height: 80px; margin-bottom: 12px; break-inside: avoid;
|
||||
min-height: 80px; margin-bottom: 0px; break-inside: avoid;
|
||||
user-select: none; -webkit-user-select: none;
|
||||
border: 2px solid var(--border-color, #333);
|
||||
color: var(--comfy-input-color, var(--fg-color, #eee));
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfy-sidebar"
|
||||
description = "A custom queue sidebar and image comparison extension that reintroduces and improves a sidebar queue menu and unified compact topbar layout for ComfyUI."
|
||||
version = "1.5.1"
|
||||
version = "1.6.1"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user