refactor: align layout, settings, and node hooks with native ComfyUI frontend API

This commit is contained in:
m0rtus59
2026-08-10 22:00:17 +05:00
parent d0424d0d05
commit 9797d1ac7a
6 changed files with 221 additions and 123 deletions
+1 -13
View File
@@ -1,20 +1,8 @@
# Standard ComfyUI extension loader
from server import PromptServer
from aiohttp import web
import uuid
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# Tells ComfyUI to serve and load the contents of the js/ folder
WEB_DIRECTORY = "./js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
# Create a unique session ID for this backend boot to track server restarts
server_session_id = str(uuid.uuid4())
# Register a custom route to check for backend restarts
@PromptServer.instance.routes.get("/classic-sidebar/session")
async def get_session_id(request):
return web.json_response({"session_id": server_session_id})
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+30 -37
View File
@@ -97,46 +97,26 @@ button:has([data-pc-name="avatar"]),
}
`;
/**
* Native settings synchronization without direct DOM element querying or simulated clicks.
*/
export function syncStockHistoryAndProgressSettings(enable) {
if (!app.ui?.settings) return;
const dockedVal = !!enable;
const progressVal = !enable;
const QPOV2_ID = "Comfy.Queue.QPOV2";
try {
app.ui.settings.setSettingValue(QPOV2_ID, dockedVal);
localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal));
} catch (e) {}
try {
const qpovContainer = document.querySelector('[data-setting-id="Comfy.Queue.QPOV2"]') ||
document.getElementById(QPOV2_ID)?.closest('.setting-item, tr, div');
if (qpovContainer) {
const toggle = qpovContainer.querySelector('input[type="checkbox"], [role="switch"], .p-toggleswitch');
if (toggle) {
const isChecked = toggle.checked ||
toggle.getAttribute('data-p-checked') === 'true' ||
toggle.classList.contains('p-toggleswitch-checked');
if (isChecked !== dockedVal) toggle.click();
}
}
} catch (e) {}
const progressKey = "Comfy.Queue.ShowRunProgressBar";
try {
app.ui.settings.setSettingValue(progressKey, progressVal);
localStorage.setItem(progressKey, JSON.stringify(progressVal));
} catch (e) {}
try {
const progressBarBtn = document.querySelector('[data-testid="show-run-progress-bar-action"]');
if (progressBarBtn) {
const isChecked = !!progressBarBtn.querySelector('[class*="lucide--check"], .icon-\\[lucide--check\\]');
if (isChecked !== progressVal) progressBarBtn.click();
}
} catch (e) {}
// Use official extensionManager setting API if present, with fallback to app.ui.settings
if (app.extensionManager?.setting) {
try { app.extensionManager.setting.set(QPOV2_ID, dockedVal); } catch (e) {}
try { app.extensionManager.setting.set(progressKey, progressVal); } catch (e) {}
} else if (app.ui?.settings) {
try { app.ui.settings.setSettingValue(QPOV2_ID, dockedVal); } catch (e) {}
try { app.ui.settings.setSettingValue(progressKey, progressVal); } catch (e) {}
}
try { localStorage.setItem(QPOV2_ID, JSON.stringify(dockedVal)); } catch (e) {}
try { localStorage.setItem(progressKey, JSON.stringify(progressVal)); } catch (e) {}
try { window.dispatchEvent(new Event("storage")); } catch (e) {}
}
@@ -144,8 +124,12 @@ export function applyClassicLayout(enable, updateSetting = false) {
let styleEl = document.getElementById(STYLE_ID);
if (enable) {
if (updateSetting && app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
if (updateSetting) {
if (app.extensionManager?.setting) {
app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
} else if (app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Topbar");
}
}
if (!styleEl) {
styleEl = document.createElement("style");
@@ -155,8 +139,12 @@ export function applyClassicLayout(enable, updateSetting = false) {
styleEl.textContent = CLASSIC_LAYOUT_CSS_MEDIA;
} else {
if (styleEl) styleEl.remove();
if (updateSetting && app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
if (updateSetting) {
if (app.extensionManager?.setting) {
app.extensionManager.setting.set("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
} else if (app.ui?.settings) {
app.ui.settings.setSettingValue("Comfy.Workflow.WorkflowTabsPosition", "Sidebar");
}
}
}
}
@@ -166,6 +154,11 @@ let domObserver = null;
let syncScheduled = false;
function findOriginalButton() {
const byTestId = document.querySelector('[data-testid="properties-panel-toggle"]');
if (byTestId && !byTestId.classList.contains("comfy-sidebar-custom-properties-toggle")) {
return byTestId;
}
const icons = document.querySelectorAll('[class*="lucide--panel-right"], [class*="lucide--panel-left"], [class*="lucide--panel-bottom"]');
for (const icon of icons) {
const btn = icon.closest('button, [role="button"], .comfyui-menu-item, .p-button');
+6
View File
@@ -115,6 +115,12 @@ const concludeRun = async (pid, statusStr) => {
export function setupApiListeners() {
api.addEventListener("status", syncQueue);
api.addEventListener("reconnected", async () => {
console.log("Comfy Sidebar: Server reconnected, syncing state and history");
await initSessionAndHistory();
await syncQueue();
});
api.addEventListener("execution_start", (e) => {
if (app.ui.settings.getSettingValue("Comfy Sidebar.Auto Clear Interrupted") ?? false) {
const toDelete = [];
+111 -26
View File
@@ -6,6 +6,8 @@ import { setupApiListeners, initSessionAndHistory, syncQueue, setUIDependencies
import { applyClassicLayout, setupPropertiesPanelToggleFix, syncClassicLayout, syncStockHistoryAndProgressSettings } from "./layout.js";
let isInitialized = false;
let activeKeydownHandler = null;
let nodeDOMObserver = null;
// Helper to scan node container for the exact text element rendering the title (Vue Mode)
function findHeaderByText(parent, text) {
@@ -24,7 +26,12 @@ function findHeaderByText(parent, text) {
}
function syncNodeVueBadge(node, isIgnored) {
const nodeEl = document.querySelector(`.comfy-node[data-node-id="${node.id}"], [data-node-id="${node.id}"]`);
// Robust multi-selector search for Vue node containers across different frontend versions
const nodeEl = document.querySelector(
`.comfy-node[data-node-id="${node.id}"], ` +
`[data-node-id="${node.id}"], ` +
`[data-id="${node.id}"]`
);
if (!nodeEl) return;
const titleTextNode = findHeaderByText(nodeEl, node.title || node.type);
@@ -92,6 +99,40 @@ export function syncAllNodeBadges() {
});
}
function setupVueNodeObserver() {
if (nodeDOMObserver) nodeDOMObserver.disconnect();
let timeoutId = null;
nodeDOMObserver = new MutationObserver((mutations) => {
let hasNodeMutation = false;
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType === 1) {
if (node.hasAttribute?.("data-node-id") ||
node.classList?.contains("comfy-node") ||
node.querySelector?.('[data-node-id], .comfy-node')) {
hasNodeMutation = true;
break;
}
}
}
if (hasNodeMutation) break;
}
if (hasNodeMutation) {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
syncAllNodeBadges();
}, 50);
}
});
nodeDOMObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function toggleIgnoreActiveNode() {
const canvas = app.canvas;
if (!canvas) return;
@@ -188,6 +229,33 @@ app.registerExtension({
});
},
commands: [
{
id: "ComfySidebar.ToggleSidebar",
label: "Toggle Comfy Queue Sidebar",
function: () => {
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
},
{
id: "ComfySidebar.ToggleIgnoreNode",
label: "Toggle Ignore Selected Node(s) in Queue",
function: () => toggleIgnoreActiveNode()
}
],
nodeCreated(node) {
if (node && node.properties?.ignoreInQueue) {
node.boxcolor = "#ff3333";
syncNodeVueBadge(node, true);
}
},
afterConfigureGraph() {
requestAnimationFrame(() => syncAllNodeBadges());
},
async setup() {
if (!app.extensionManager || !app.extensionManager.registerSidebarTab) return;
@@ -215,41 +283,42 @@ app.registerExtension({
await initSessionAndHistory();
// Keyboard shortcuts listener
document.addEventListener("keydown", (e) => {
// Watch for Vue DOM node mounting events (e.g. LiteGraph ↔ Vue mode switches)
setupVueNodeObserver();
// Remove any existing keydown listener from previous hot-reload
if (activeKeydownHandler) {
document.removeEventListener("keydown", activeKeydownHandler, true);
activeKeydownHandler = null;
}
// Clean, direct keyboard listener
activeKeydownHandler = (e) => {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable || activeEl.tagName === "SELECT")) return;
if (activeEl && (
activeEl.tagName === "INPUT" ||
activeEl.tagName === "TEXTAREA" ||
activeEl.isContentEditable ||
activeEl.tagName === "SELECT"
)) return;
// Toggle sidebar with Q
if (e.key.toLowerCase() === "q" && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault(); e.stopPropagation();
e.preventDefault();
e.stopPropagation();
const ourBtn = findOurSidebarButton();
if (ourBtn) ourBtn.click();
}
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.stopPropagation();
// Toggle ignore node with Ctrl+Q or Cmd+Q (Mac)
if (e.key.toLowerCase() === "q" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
toggleIgnoreActiveNode();
}
}, true);
// Event hooks for node additions and workflow loading (eliminates continuous polling loops)
if (app.graph) {
const originalConfigure = app.graph.configure;
app.graph.configure = function() {
const res = originalConfigure ? originalConfigure.apply(this, arguments) : undefined;
requestAnimationFrame(() => syncAllNodeBadges());
return res;
};
};
const originalOnNodeAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function(node) {
if (originalOnNodeAdded) originalOnNodeAdded.apply(this, arguments);
if (node && node.properties?.ignoreInQueue) {
node.boxcolor = "#ff3333";
syncNodeVueBadge(node, true);
}
};
}
document.addEventListener("keydown", activeKeydownHandler, true);
// Apply sidebar override when initializing
applySidebarOverride();
@@ -262,5 +331,21 @@ app.registerExtension({
type: "custom",
render: (el) => { el.appendChild(sidebarContainer); }
});
},
destroy() {
if (nodeDOMObserver) {
nodeDOMObserver.disconnect();
nodeDOMObserver = null;
}
if (activeKeydownHandler) {
document.removeEventListener("keydown", activeKeydownHandler, true);
activeKeydownHandler = null;
}
if (app.extensionManager?.unregisterSidebarTab) {
app.extensionManager.unregisterSidebarTab("classic-comfy-sidebar");
}
}
});
+57
View File
@@ -97,6 +97,63 @@ export function injectStyles() {
color: #ffffff !important;
box-shadow: 0 0 8px rgba(220, 53, 69, 0.6) !important;
}
/* Header Icons & Header Action Buttons */
.comfy-sidebar-header-btn {
background: transparent;
color: var(--desc-color, #aaa);
border: 1px solid var(--border-color, #555);
border-radius: 3px;
padding: 4px 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s ease-in-out;
}
.comfy-sidebar-header-btn:hover {
border-color: var(--fg-color, #eee);
color: var(--fg-color, #eee);
}
.comfy-sidebar-icon-btn {
cursor: pointer;
font-size: 13px;
opacity: 0.6;
transition: opacity 0.15s ease-in-out;
}
.comfy-sidebar-icon-btn:hover {
opacity: 1;
}
/* Scroll To Top Floating Button */
.comfy-sidebar-scroll-top-btn {
position: absolute;
bottom: 12px;
right: 12px;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(30, 30, 30, 0.9);
color: #eee;
border: 1px solid var(--border-color, #555);
cursor: pointer;
font-size: 13px;
display: none;
align-items: center;
justify-content: center;
z-index: 999;
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
transition: all 0.15s ease-in-out;
opacity: 0.85;
}
.comfy-sidebar-scroll-top-btn:hover {
opacity: 1;
background: var(--p-primary-color, #3b82f6);
color: #fff;
border-color: var(--p-primary-color, #3b82f6);
}
`;
document.head.appendChild(style);
}
+16 -47
View File
@@ -162,11 +162,8 @@ export function setupSidebarUI() {
Object.assign(titleGroup.style, { display: "flex", alignItems: "center", gap: "8px" });
const searchIcon = document.createElement("span");
searchIcon.className = "pi pi-search";
searchIcon.className = "pi pi-search comfy-sidebar-icon-btn";
searchIcon.title = "Search History";
Object.assign(searchIcon.style, { cursor: "pointer", fontSize: "13px", opacity: "0.6", transition: "opacity 0.15s ease-in-out" });
searchIcon.onmouseenter = () => searchIcon.style.opacity = "1";
searchIcon.onmouseleave = () => searchIcon.style.opacity = "0.6";
const title = document.createElement("h3");
title.textContent = "Queue";
@@ -181,12 +178,8 @@ export function setupSidebarUI() {
const createActionBtn = (iconClass, tooltip, hoverColor, onClickFn) => {
const btn = document.createElement("button");
btn.className = iconClass;
btn.className = `${iconClass} comfy-sidebar-header-btn`;
btn.title = tooltip;
Object.assign(btn.style, {
background: "transparent", color: "var(--desc-color, #aaa)", border: "1px solid var(--border-color, #555)",
borderRadius: "3px", padding: "4px 8px", cursor: "pointer", fontSize: "13px", transition: "all 0.15s ease-in-out"
});
let timeout = null, isPending = false;
const reset = () => {
@@ -195,10 +188,7 @@ export function setupSidebarUI() {
if (timeout) { clearTimeout(timeout); timeout = null; }
};
btn.onmouseenter = () => { if (!isPending) { btn.style.borderColor = "var(--fg-color, #eee)"; btn.style.color = "var(--fg-color, #eee)"; } };
btn.onmouseleave = () => { if (!isPending) { btn.style.borderColor = "var(--border-color, #555)"; btn.style.color = "var(--desc-color, #aaa)"; } };
btn.onclick = async (ev) => {
btn.addEventListener("click", async (ev) => {
ev.stopPropagation();
if (!isPending) {
isPending = true;
@@ -208,7 +198,7 @@ export function setupSidebarUI() {
reset();
await onClickFn();
}
};
});
return btn;
};
@@ -251,59 +241,38 @@ export function setupSidebarUI() {
Object.assign(searchInput, { type: "text", placeholder: "Filter by text, images, nodes..." });
Object.assign(searchInput.style, { flex: "1", background: "transparent", border: "none", outline: "none", color: "var(--comfy-input-color, var(--fg-color, #eee))", fontSize: "11px", padding: "0" });
const clearSearchBtn = document.createElement("span");
clearSearchBtn.className = "pi pi-times";
clearSearchBtn.className = "pi pi-times comfy-sidebar-icon-btn";
clearSearchBtn.title = "Clear & Close Search";
Object.assign(clearSearchBtn.style, { cursor: "pointer", fontSize: "11px", opacity: "0.6", marginLeft: "6px", transition: "opacity 0.15s ease" });
clearSearchBtn.onmouseenter = () => clearSearchBtn.style.opacity = "1"; clearSearchBtn.onmouseleave = () => clearSearchBtn.style.opacity = "0.6";
Object.assign(clearSearchBtn.style, { marginLeft: "6px" });
searchContainer.appendChild(searchInputIcon); searchContainer.appendChild(searchInput); searchContainer.appendChild(clearSearchBtn);
header.appendChild(standardHeader); header.appendChild(searchContainer); State.sidebarContainer.appendChild(header);
searchIcon.onclick = (e) => { e.stopPropagation(); standardHeader.style.display = "none"; searchContainer.style.display = "flex"; searchInput.focus(); };
searchIcon.addEventListener("click", (e) => { e.stopPropagation(); standardHeader.style.display = "none"; searchContainer.style.display = "flex"; searchInput.focus(); });
const closeSearch = () => { searchInput.value = ""; State.currentSearchQuery = ""; searchContainer.style.display = "none"; standardHeader.style.display = "flex"; renderDOM(); };
clearSearchBtn.onclick = (e) => { e.stopPropagation(); closeSearch(); };
searchInput.onkeydown = (e) => { if (e.key === "Escape") closeSearch(); };
searchInput.oninput = () => { State.currentSearchQuery = searchInput.value.trim(); renderDOM(); };
clearSearchBtn.addEventListener("click", (e) => { e.stopPropagation(); closeSearch(); });
searchInput.addEventListener("keydown", (e) => { if (e.key === "Escape") closeSearch(); });
searchInput.addEventListener("input", () => { State.currentSearchQuery = searchInput.value.trim(); renderDOM(); });
State.cardStack = document.createElement("div");
Object.assign(State.cardStack.style, { flex: "1", overflowY: "visible", display: "block", paddingBottom: "28px" });
State.sidebarContainer.appendChild(State.cardStack);
scrollToTopBtnEl = document.createElement("button");
scrollToTopBtnEl.className = "pi pi-chevron-up";
scrollToTopBtnEl.className = "pi pi-chevron-up comfy-sidebar-scroll-top-btn";
scrollToTopBtnEl.title = "Scroll to Top";
Object.assign(scrollToTopBtnEl.style, {
position: "absolute", bottom: "12px", right: "12px", width: "32px", height: "32px",
borderRadius: "50%", background: "rgba(30, 30, 30, 0.9)", color: "#eee",
border: "1px solid var(--border-color, #555)", cursor: "pointer", fontSize: "13px",
display: "none", alignItems: "center", justifyContent: "center", zIndex: "999",
boxShadow: "0 4px 12px rgba(0,0,0,0.6)", transition: "all 0.15s ease-in-out", opacity: "0.85"
});
scrollToTopBtnEl.onmouseenter = () => {
scrollToTopBtnEl.style.opacity = "1";
scrollToTopBtnEl.style.background = "var(--p-primary-color, #3b82f6)";
scrollToTopBtnEl.style.color = "#fff";
scrollToTopBtnEl.style.borderColor = "var(--p-primary-color, #3b82f6)";
};
scrollToTopBtnEl.onmouseleave = () => {
scrollToTopBtnEl.style.opacity = "0.85";
scrollToTopBtnEl.style.background = "rgba(30, 30, 30, 0.9)";
scrollToTopBtnEl.style.color = "#eee";
scrollToTopBtnEl.style.borderColor = "var(--border-color, #555)";
};
scrollToTopBtnEl.onclick = (e) => {
scrollToTopBtnEl.addEventListener("click", (e) => {
e.stopPropagation();
const scrollEl = getScrollContainer();
if (scrollEl) {
scrollEl.scrollTo({ top: 0, behavior: "smooth" });
}
};
});
State.sidebarContainer.appendChild(scrollToTopBtnEl);
State.sidebarContainer.onclick = (e) => {
State.sidebarContainer.addEventListener("click", (e) => {
if (State.activeSubmenuPromptId || State.activeSubmenuBatchImages) {
if (e.target.closest('img, video, .comfy-sidebar-card-timer, .pi-times, .comfy-sidebar-left-hover-btn, .comfy-sidebar-queue-cancel-btn, button, span')) return;
State.activeSubmenuPromptId = null;
@@ -313,7 +282,7 @@ export function setupSidebarUI() {
document.removeEventListener("click", handleGlobalClick, true);
globalClickRegistered = false;
}
};
});
new ResizeObserver((entries) => {
const threshold = app.ui.settings.getSettingValue("Comfy Sidebar.Grid Columns Threshold") ?? 350;
@@ -471,7 +440,7 @@ function renderCardImages(cardObj, state, keepAspect) {
showFullscreenPreview([src], ev.shiftKey);
};
// Auto-prune card if file was deleted by startup temp cleanup script
// Auto-prune card if file was deleted
mediaEl.onerror = () => {
if (src && !src.startsWith("blob:")) {
deletePromptState(state.pid);