From 70c8e64c4e31a7817df154fee73599a4ec3f8e00 Mon Sep 17 00:00:00 2001 From: m0rtus59 Date: Fri, 18 Sep 2026 21:04:55 +0500 Subject: [PATCH] More hardening. File deletion logic improvements --- .gitignore | 3 +- __init__.py | 102 +++++++++-------------------------- js/queue/executionTracker.js | 2 +- js/sidebar.js | 6 +++ js/ui/cardRenderer.js | 31 ++++++++--- js/ui/mediaActions.js | 16 ++++-- js/ui/mediaPreview.js | 12 +++-- js/ui/sidebarView.js | 17 +++++- js/utils/nodeRenumber.js | 11 +++- js/utils/viewer3d.js | 26 ++++----- pyproject.toml | 6 ++- requirements.txt | 1 + 12 files changed, 116 insertions(+), 117 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 14cc7bc..7bfcd7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ __pycache__/ -*.pyc -js/vendor/ \ No newline at end of file +*.pyc \ No newline at end of file diff --git a/__init__.py b/__init__.py index f98c4ac..7a636b4 100644 --- a/__init__.py +++ b/__init__.py @@ -60,9 +60,9 @@ def get_all_candidate_roots(subfolder="", folder_type="output"): return unique -def resolve_existing_path(filename="", subfolder="", folder_type="output"): +def resolve_existing_path(filename="", subfolder="", folder_type="output", candidate_roots=None): """Finds the actual location of a file or folder across all candidate root directories.""" - candidates = get_all_candidate_roots(subfolder, folder_type) + candidates = candidate_roots if candidate_roots is not None else get_all_candidate_roots(subfolder, folder_type) if filename: for root in candidates: @@ -97,7 +97,7 @@ def open_file_in_os(target_path): # Passed as a single argument so Windows Explorer highlights the file properly subprocess.Popen(["explorer", f"/select,{norm_path}"]) else: - os.startfile(norm_path) + subprocess.Popen(["explorer", norm_path]) elif system == "Darwin": # macOS if not os.path.exists(target_path): @@ -146,19 +146,34 @@ async def delete_file_handler(request): if not filename: return web.json_response({"error": "No filename provided"}, status=400) - target_file, root = resolve_existing_path(filename, subfolder, folder_type) + # Strictly restrict deletion to generated output and temp files (protects models/checkpoints) + delete_roots = [ + folder_paths.get_output_directory(), + folder_paths.get_temp_directory() + ] + delete_roots = [os.path.realpath(r) for r in delete_roots if r and os.path.exists(r)] + + target_file, root = resolve_existing_path(filename, subfolder, folder_type, candidate_roots=delete_roots) if not target_file or not os.path.isfile(target_file) or not is_path_safe(root, target_file): - return web.json_response({"error": "File not found or unsafe path"}, status=404) + return web.json_response({"error": "File not found or outside output directory"}, status=404) # Try moving to Recycle Bin / Trash first, fallback to os.remove - try: - import send2trash - send2trash.send2trash(target_file) - except Exception: + permanent = bool(data.get("permanent", False)) + action = "deleted" + if not permanent: + try: + import send2trash + send2trash.send2trash(target_file) + action = "trashed" + except Exception: + os.remove(target_file) + action = "deleted" + else: os.remove(target_file) + action = "deleted" - return web.json_response({"success": True, "deleted": target_file}) + return web.json_response({"success": True, "deleted": target_file, "action": action}) except Exception as e: return web.json_response({"error": str(e)}, status=500) @@ -185,69 +200,4 @@ async def promote_output_handler(request): shutil.copy2(target_file, dest_path) return web.json_response({"name": filename}) except Exception as e: - return web.json_response({"error": str(e)}, status=500) - - -# ============================================================================ -# Local Three.js Proxy & Offline Cache (Bypasses CSP & --disable-api-nodes) -# ============================================================================ -SIDEBAR_CACHE_DIR = os.path.join(os.path.dirname(__file__), "js", "vendor", "cache") - -@PromptServer.instance.routes.get("/comfy-sidebar/three-proxy/{path:.*}") -async def three_proxy_handler(request): - raw_path = request.match_info.get("path", "").lstrip("/") - if not raw_path: - return web.Response(status=404) - - query = request.query_string - full_req = f"{raw_path}?{query}" if query else raw_path - - os.makedirs(SIDEBAR_CACHE_DIR, exist_ok=True) - import re - 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 - if os.path.isfile(cache_file) and os.path.getsize(cache_file) > 0: - content_type = "application/wasm" if raw_path.endswith(".wasm") else "application/javascript" - return web.FileResponse(cache_file, headers={"Content-Type": content_type}) - - # 2. Otherwise download via Python (not blocked by browser CSP) - 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)"}) - with urllib.request.urlopen(req, timeout=20) as resp: - raw_data = resp.read() - - if raw_path.endswith((".wasm", ".bin", ".png", ".jpg")): - with open(cache_file, "wb") as f: - f.write(raw_data) - return web.Response(body=raw_data, content_type="application/octet-stream") - - content = raw_data.decode("utf-8", errors="replace") - # Rewrite absolute esm.sh paths to route through our local 'self' proxy - content = content.replace("https://esm.sh/", "/comfy-sidebar/three-proxy/") - # Rewrite root-relative module paths (e.g. from "/v135/...") - content = re.sub( - r'((?:from|import)\s*["\'])/(v\d+|[a-zA-Z0-9@_.~-]+/)', - r'\1/comfy-sidebar/three-proxy/\2', - content - ) - - with open(cache_file, "w", encoding="utf-8") as f: - f.write(content) - - return web.Response(text=content, content_type="application/javascript") - except Exception as e: - return web.Response(text=f"/* Proxy error fetching {url}: {e} */", status=502, content_type="application/javascript") \ No newline at end of file + return web.json_response({"error": str(e)}, status=500) \ No newline at end of file diff --git a/js/queue/executionTracker.js b/js/queue/executionTracker.js index 029862b..9438bda 100644 --- a/js/queue/executionTracker.js +++ b/js/queue/executionTracker.js @@ -118,7 +118,7 @@ export function setupExecutionTracker(onTargetedProgressUpdate) { prompt.images = nodeImgs; // If fullscreen is open, smoothly transition to the final saved image const finalImg = nodeImgs[0]; - const finalSrc = finalImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(finalImg.filename)}&type=${finalImg.type || 'output'}&subfolder=${encodeURIComponent(finalImg.subfolder || '')}`); + const finalSrc = finalImg.url || (window.location.origin + `/view?filename=${encodeURIComponent(finalImg.filename)}&type=${finalImg.type || 'output'}&subfolder=${encodeURIComponent(finalImg.subfolder || '')}&rand=${encodeURIComponent(pid)}`); updateActiveComparisonPreview(pid, finalSrc); } const nodeTexts = findTextsInOutputs({ [e.detail.node]: e.detail.output }, prompt.workflow); diff --git a/js/sidebar.js b/js/sidebar.js index eb01176..b9e6037 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -44,6 +44,12 @@ function registerSettings() { type: "boolean", defaultValue: false }); + app.ui.settings.addSetting({ + id: "Comfy Sidebar.Permanent Delete on Disk Deletion", + name: "Permanently delete files on disk deletion (bypasses Recycle Bin / Trash)", + type: "boolean", + defaultValue: false + }); const sidebarTabs = ["Assets", "Nodes", "Models", "Workflows", "Apps", "Templates"]; sidebarTabs.forEach(tab => { diff --git a/js/ui/cardRenderer.js b/js/ui/cardRenderer.js index 0d49a03..4dc7e46 100644 --- a/js/ui/cardRenderer.js +++ b/js/ui/cardRenderer.js @@ -8,6 +8,7 @@ import { getRunOutputs, extractWorkflowFromPng, isImageFormat } from "../utils/u import { showFullscreenPreview } from "../utils/comparison.js"; import { stopAllAudioPlayback } from "./audioController.js"; import { deleteHistoryItem, cancelPendingTask, interruptActive } from "../queue/queueService.js"; +import { showNotification } from "../utils/nodeRenumber.js"; export const cardPool = new Map(); @@ -31,6 +32,8 @@ export function syncCardButtonVisibility(cardObj, state) { if (cardObj.leftHoverPanel) cardObj.leftHoverPanel.style.removeProperty("display"); } + const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : ""; + // Check if the card has an actual verified file saved on disk const hasRealDiskFiles = state.images && state.images.length > 0 && state.images.some(img => img.filename && !img.isFallback && (!img.url || !img.url.startsWith("blob:"))); @@ -47,7 +50,7 @@ export function syncCardButtonVisibility(cardObj, state) { ev.stopPropagation(); state.images.forEach(img => { const a = document.createElement("a"); - a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; + a.href = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; a.download = img.filename || "output"; a.click(); }); @@ -64,7 +67,7 @@ export function syncCardButtonVisibility(cardObj, state) { cardObj.btnCopy.style.display = "inline-flex"; cardObj.btnCopy.onclick = async (ev) => { ev.stopPropagation(); - const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}`; + const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}${randParam}`; const success = await copyImageToClipboard(src); if (success) { cardObj.btnCopy.className = "pi pi-check comfy-sidebar-card-action-btn"; @@ -88,7 +91,7 @@ export function syncCardButtonVisibility(cardObj, state) { ev.stopPropagation(); let wf = state.workflow; if (!wf && currentImg) { - const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}`; + const src = currentImg.url ? currentImg.url : `/view?filename=${encodeURIComponent(currentImg.filename)}&type=${currentImg.type || 'output'}&subfolder=${encodeURIComponent(currentImg.subfolder || '')}${randParam}`; wf = await extractWorkflowFromPng(src); if (wf) state.workflow = wf; } @@ -161,7 +164,7 @@ export function syncCardButtonVisibility(cardObj, state) { // 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 || '')}`; + const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; if (!src.startsWith("blob:")) { fetch(src, { method: "HEAD" }).then(res => { if (res.status === 404) { @@ -197,7 +200,7 @@ export function syncCardButtonVisibility(cardObj, state) { 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 || '')}`; + const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; if (!src.startsWith("blob:")) { try { const res = await fetch(src, { method: "HEAD" }); @@ -248,7 +251,8 @@ export function updateCardProgressTargeted(cardObj, progress, activeNodeName, sh // Fast-path: update live preview image directly on active card if (state && state.images && state.images.length > 0) { const firstImg = state.images[0]; - const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}` : null); + const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : ""; + const previewUrl = firstImg.url || (firstImg.filename ? `/view?filename=${encodeURIComponent(firstImg.filename)}&type=${firstImg.type || 'output'}&subfolder=${encodeURIComponent(firstImg.subfolder || '')}${randParam}` : null); if (previewUrl && cardObj.firstImgElement && cardObj.firstImgElement.tagName === "IMG") { const currentSrc = cardObj.firstImgElement.getAttribute("src") || cardObj.firstImgElement.src; if (currentSrc !== previewUrl && !cardObj.firstImgElement.src.endsWith(previewUrl)) { @@ -463,8 +467,21 @@ export function getOrCreateCard(state, callbacks = {}) { const allOutputs = getRunOutputs(liveState.nodeOutputs, liveState.workflow); allOutputs.forEach(out => (out.images || []).forEach(addFile)); + let deletedCount = 0; + let lastAction = "trashed"; for (const fileItem of filesToDelete) { - await deleteFileOnServer(fileItem); + const action = await deleteFileOnServer(fileItem); + if (action) { + deletedCount++; + lastAction = action; + } + } + + if (deletedCount > 0) { + const msg = lastAction === "trashed" + ? `Moved ${deletedCount} file${deletedCount > 1 ? "s" : ""} to Recycle Bin.` + : `Permanently deleted ${deletedCount} file${deletedCount > 1 ? "s" : ""}.`; + showNotification(msg, "success", "Delete Files"); } } diff --git a/js/ui/mediaActions.js b/js/ui/mediaActions.js index 1a27197..22bb982 100644 --- a/js/ui/mediaActions.js +++ b/js/ui/mediaActions.js @@ -50,8 +50,11 @@ export async function openFileOrFolder(img) { a.remove(); } +import { app } from "/scripts/app.js"; + export async function deleteFileOnServer(fileItem) { - if (!fileItem || !fileItem.filename) return false; + if (!fileItem || !fileItem.filename) return null; + const isPermanent = app.ui?.settings?.getSettingValue?.("Comfy Sidebar.Permanent Delete on Disk Deletion") ?? false; try { const res = await fetch("/comfy-sidebar/delete-file", { method: "POST", @@ -59,13 +62,18 @@ export async function deleteFileOnServer(fileItem) { body: JSON.stringify({ filename: fileItem.filename, subfolder: fileItem.subfolder || "", - type: fileItem.type || "output" + type: fileItem.type || "output", + permanent: isPermanent }) }); - return res.ok; + if (res.ok) { + const data = await res.json(); + return data.action || (isPermanent ? "deleted" : "trashed"); + } + return null; } catch (e) { console.error("Comfy Sidebar: Failed to delete file on server", e); - return false; + return null; } } diff --git a/js/ui/mediaPreview.js b/js/ui/mediaPreview.js index 2c54556..6343fd9 100644 --- a/js/ui/mediaPreview.js +++ b/js/ui/mediaPreview.js @@ -7,7 +7,8 @@ import { PromptStatus } from "../core/constants.js"; export function render3DCardPreview(cardObj, wrapper, src, img, state) { let preview3D = wrapper.querySelector(".comfy-sidebar-3d-wrapper"); - const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; + const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : ""; + const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; const imageAssets = state?.images?.filter(i => isImageFormat(i.filename || i.url)) || []; const lastImageAsset = imageAssets.length > 0 ? imageAssets[imageAssets.length - 1] : null; @@ -92,7 +93,8 @@ export function render3DCardPreview(cardObj, wrapper, src, img, state) { export function renderAudioCardPreview(cardObj, wrapper, src, img, state) { let previewAudio = wrapper.querySelector(".comfy-sidebar-audio-wrapper"); - const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; + const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : ""; + const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; const filename = img.filename || getFilenameFromUrl(src) || "audio.wav"; const ext = filename.split('.').pop().toUpperCase(); @@ -277,7 +279,8 @@ export function renderGenericFileCardPreview(cardObj, wrapper, src, img, state) 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 randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : ""; + const fullUrl = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; const filename = img.filename || getFilenameFromUrl(src) || "output_file"; const ext = (filename.split('.').pop() || "FILE").toUpperCase(); const folderName = img.subfolder ? `${img.subfolder}/` : (img.type || "output"); @@ -368,7 +371,8 @@ export function renderCardImages(cardObj, state, onNavigateBatch) { return; } - const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}`; + const randParam = state?.pid ? `&rand=${encodeURIComponent(state.pid)}` : ""; + const src = img.url ? img.url : window.location.origin + `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type || 'output'}&subfolder=${encodeURIComponent(img.subfolder || '')}${randParam}`; const isVideo = isVideoFormat(src) || isVideoFormat(img.filename); const is3D = is3DFormat(src) || is3DFormat(img.filename); const isAudio = isAudioFormat(src) || isAudioFormat(img.filename); diff --git a/js/ui/sidebarView.js b/js/ui/sidebarView.js index a877a6a..a44cd4c 100644 --- a/js/ui/sidebarView.js +++ b/js/ui/sidebarView.js @@ -512,7 +512,11 @@ export function renderSidebar() { resetDelete(); if (shouldDeleteFromDisk && img && img.filename) { - await deleteFileOnServer(img); + const action = await deleteFileOnServer(img); + if (action) { + const msg = action === "trashed" ? "Moved file to Recycle Bin." : "Permanently deleted file."; + showNotification(msg, "success", "Delete File"); + } } const pid = batchInfo.parentPromptId || batchInfo.pid; @@ -779,8 +783,17 @@ export function renderSidebar() { resetDelete(); if (shouldDeleteFromDisk && out.images) { + let count = 0; + let lastAct = "trashed"; for (const imgItem of out.images) { - await deleteFileOnServer(imgItem); + const action = await deleteFileOnServer(imgItem); + if (action) { count++; lastAct = action; } + } + if (count > 0) { + const msg = lastAct === "trashed" + ? `Moved ${count} file${count > 1 ? "s" : ""} to Recycle Bin.` + : `Permanently deleted ${count} file${count > 1 ? "s" : ""}.`; + showNotification(msg, "success", "Delete Files"); } } diff --git a/js/utils/nodeRenumber.js b/js/utils/nodeRenumber.js index a1d7e68..bea90d9 100644 --- a/js/utils/nodeRenumber.js +++ b/js/utils/nodeRenumber.js @@ -1,8 +1,8 @@ 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"; +export function showNotification(message, type = "success", title = null) { + const summary = title || (type === "error" ? "Error" : type === "warn" ? "Warning" : "Comfy Sidebar"); if (app.extensionManager?.toast?.add) { app.extensionManager.toast.add({ @@ -61,6 +61,13 @@ export async function renumberNodesTopologically() { return; } + const confirmed = confirm( + "Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\n" + + "This will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\n" + + "Do you want to proceed?" + ); + if (!confirmed) return; + try { // Compute LiteGraph's topological execution order if (typeof app.graph.computeExecutionOrder === "function") { diff --git a/js/utils/viewer3d.js b/js/utils/viewer3d.js index 470092b..4d8a062 100644 --- a/js/utils/viewer3d.js +++ b/js/utils/viewer3d.js @@ -36,19 +36,9 @@ async function getThreeLibs() { }; try { - let loaded = null; - // 1. Try local Python proxy on 'self' (bypasses CSP & --disable-api-nodes) - try { - loaded = await loadFromBase("/comfy-sidebar/three-proxy/"); - window.__COMFY_SIDEBAR_THREE_BASE = "/comfy-sidebar/three-proxy/"; - } catch (proxyErr) { - // 2. Fall back to direct CDN if proxy is unreachable - console.warn("Comfy Sidebar: Proxy unavailable, trying direct CDN...", proxyErr); - loaded = await loadFromBase("https://esm.sh/"); - window.__COMFY_SIDEBAR_THREE_BASE = "https://esm.sh/"; - } - - const { three, controls, gltf, obj, stl, ply } = loaded; + const base = "https://esm.sh/"; + window.__COMFY_SIDEBAR_THREE_BASE = base; + const { three, controls, gltf, obj, stl, ply } = await loadFromBase(base); cachedThreeLibs = { ...three, OrbitControls: controls.OrbitControls || controls.default || controls, @@ -232,7 +222,7 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () loadingSpinner.style.display = "flex"; loadingSpinner.innerHTML = ` Exporting ${format}...`; - const base = window.__COMFY_SIDEBAR_THREE_BASE || "/comfy-sidebar/three-proxy/"; + const base = window.__COMFY_SIDEBAR_THREE_BASE || "https://esm.sh/"; if (format === "OBJ") { const mod = await import(`${base}three@0.170.0/examples/jsm/exporters/OBJExporter.js`); const OBJExporter = mod.OBJExporter || mod.default || mod; @@ -283,9 +273,11 @@ export function create3DViewer(baseSrc, onSwitchMedia = () => {}, onDestroy = () THREE = await getThreeLibs(); if (!THREE) { loadingSpinner.innerHTML = ` -
- Failed to load 3D engine.
- Blocked by ComfyUI Content Security Policy (CSP). Check if --disable-api-nodes is enabled in your startup arguments, or vendor Three.js locally. +
+ Failed to load 3D engine.
+ + Unable to download 3D libraries. Make sure your computer is online, or check if --disable-api-nodes is blocking external network requests. +
`; return; diff --git a/pyproject.toml b/pyproject.toml index c00b509..5944bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,11 @@ [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.6.1" +version = "1.6.2" license = { file = "LICENSE" } -dependencies = [] +dependencies = [ + "send2trash>=1.8.0", +] [project.urls] Repository = "https://github.com/m0rtus59/ComfyUI-ComfySidebar" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ce64012 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +send2trash>=1.8.0 \ No newline at end of file