Compare commits

..
3 Commits
Author SHA1 Message Date
m0rtus59 1f3c3cb598 hardening 2026-09-17 22:11:32 +05:00
m0rtus59 0238cf109d New 'reset node IDs' function to ensure the final output node is always prioritized as the primary preview 2026-09-17 02:04:43 +05:00
m0rtus59 869d3b9a28 fixes 2026-09-13 19:37:54 +05:00
10 changed files with 498 additions and 184 deletions
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+38
View File
@@ -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";
+3 -10
View File
@@ -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();
}
+2
View File
@@ -509,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();
+44 -1
View File
@@ -7,6 +7,8 @@ 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;
@@ -186,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");
@@ -220,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();
@@ -230,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);
+206
View File
@@ -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
View File
@@ -1,7 +1,7 @@
[project]
name = "comfy-sidebar"
description = "A custom queue sidebar and image comparison extension that reintroduces and improves a sidebar queue menu and unified compact topbar layout for ComfyUI."
version = "1.5.3"
version = "1.6.1"
license = { file = "LICENSE" }
dependencies = []