diff --git a/README.md b/README.md index 85caf41..f294dca 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ ![Version](https://img.shields.io/badge/version-1.0.0-blue) ![ComfyUI](https://img.shields.io/badge/ComfyUI-Compatible-green) -**HALXP-Comfy** (High Availability & Layout eXtension Pack) is a professional UI suite for ComfyUI designed to enhance workflow management. +**HALXP-Comfy** is a professional UX suite for ComfyUI designed to enhance workflow management. -This extension is the starting point for a growing collection of UI tools. Currently, it features **HALXP Focus**, a smart navigation utility, with more enhancements currently in development. +This extension is the starting point for a growing collection of tools. Currently, it features **HALXP Focus**, a way to auto-focus the current running node and , **HALXP RunMonitor** taht lets you run a script on success or error with more enhancements currently in development. **Verified working on:** Legacy UI, ComfyUI V1 (Nodes 2.0), and Portable Distributions. @@ -21,7 +21,18 @@ A smart "Focus" button injected into your main menu bar. * **Visual Feedback:** Features a status indicator (Green dot = Active) so you always know when auto-follow is engaged. * **V1 Compatible:** Works natively with the new Vue.js frontend and PrimeVue menus. -### 2. More Coming Soon +### 2. HALXP RunMonitor +A way to run a custom script on workflow success or failure +* Go in the Settings under HALXP-Comfy and set a batch file to run or anything you want +* (play a wav, open a website, etc.) separately for sucess and errors - doesn't cover static validation on launch +* ex: With my phone provider (Free Mobile in France), I have an API to send SMS so I can go do other things and get notified when my workflow ends +* batch file content example: +* start "" "https://smsapi.free-mobile.fr/sendmsg?user=XXXXXXXXXX&pass=YYYYYYYYYYY&msg=Comfy%%20Error%%20!" +* **Smart Docking:** Automatically detects the ComfyUI Manager button and injects itself seamlessly beside it. +* **Visual Feedback:** Features a status indicator (Green dot = Active) so you always know when Monitor is engaged. +* **V1 Compatible:** Works natively with the new Vue.js frontend and PrimeVue menus. + +### 3. More Coming Soon HALXP is built as a modular suite. Expect future updates to include additional layout and debugging tools to further improve your ComfyUI experience. ## 📦 Installation @@ -35,4 +46,4 @@ HALXP is built as a modular suite. Expect future updates to include additional l 1. Navigate to your `ComfyUI/custom_nodes/` directory. 2. Clone this repository: ```bash - git clone [https://github.com/HAL-XP/halxp-comfy](https://github.com/HAL-XP/halxp-comfy) \ No newline at end of file + git clone [https://github.com/YOUR_USERNAME/halxp-comfyui](https://github.com/YOUR_USERNAME/halxp-comfyui) \ No newline at end of file diff --git a/__init__.py b/__init__.py index a19508a..79305f5 100644 --- a/__init__.py +++ b/__init__.py @@ -1,12 +1,80 @@ -# HALXP - High Availability & Layout eXtension Pack -# Registers the web directory for JS extensions +import server +import subprocess +import asyncio +from aiohttp import web +# --- CONFIG STORAGE --- +halxp_config = { + "enabled": False, + "success_path": "", + "error_path": "" +} + +# --- STATE TRACKING --- +run_has_error = False + +# --- 1. API ROUTE --- +@server.PromptServer.instance.routes.post("/halxp/update_config") +async def update_halxp_config(request): + try: + data = await request.json() + halxp_config.update(data) + return web.json_response({"status": "ok"}) + except Exception as e: + print(f"[HALXP-Core] Error updating config: {e}") + return web.json_response({"status": "error"}, status=500) + +# --- 2. MESSAGE HOOK --- +original_send_json = server.PromptServer.instance.send_json + +async def intercepted_send_json(self, event, data, sid=None): + global run_has_error + + # 1. Execute Original Behavior + await original_send_json(event, data, sid) + + # 2. Check RunMonitor Logic + if halxp_config.get("enabled"): + try: + # --- START: SAFETY RESET --- + # Just in case the last run didn't clear the flag, we clear it on start too. + if event == "execution_start": + run_has_error = False + + # --- DETECT ERROR --- + elif event == "execution_error": + run_has_error = True + cmd = halxp_config.get("error_path") + if cmd and cmd.strip(): + print(f"[HALXP-RunMonitor] ❌ Workflow Error. Running: {cmd}") + subprocess.Popen(cmd, shell=True) + + # --- DETECT FINISH (SUCCESS or FAIL) --- + # 'executing' with node=None means the prompt is done. + elif event == "executing" and data.get("node") is None: + + # Only run success script if NO error occurred + if not run_has_error: + cmd = halxp_config.get("success_path") + if cmd and cmd.strip(): + print(f"[HALXP-RunMonitor] ✅ Workflow Finished. Running: {cmd}") + subprocess.Popen(cmd, shell=True) + + # --- CRITICAL FIX: ALWAYS RESET STATE HERE --- + # Once the run is finished, we must clear the error flag + # so the NEXT run starts fresh. + if run_has_error: + print(f"[HALXP-RunMonitor] 🔄 Run finished with errors. Resetting state for next run.") + run_has_error = False + + except Exception as e: + print(f"[HALXP-RunMonitor] ⚠️ Failed to execute monitoring script: {e}") + +# Apply the hook +server.PromptServer.instance.send_json = intercepted_send_json.__get__(server.PromptServer.instance, server.PromptServer) + +# --- STANDARD NODE MAPPING --- WEB_DIRECTORY = "./js" - -# Export empty dictionaries so ComfyUI doesn't warn about missing mappings NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} - -__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] - -print("\033[34m[HALXP-ComfyUI] JS Extensions Loaded successfully.\033[0m") \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/js/halxp-focus.js b/js/halxp-focus.js index a6bc8dc..e6b024d 100644 --- a/js/halxp-focus.js +++ b/js/halxp-focus.js @@ -1,183 +1,115 @@ -// \ComfyUI\custom_nodes\better-focus\js\universal_focus.js -// console.log("%c[HALXP-Comfy-Focus] Loaded", "background: purple; color: white; font-weight: bold;"); - import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; app.registerExtension({ - name: "Comfy.HALXP-ComfyUI.Focus", + name: "Comfy.HALXP.Focus", async setup() { - // --- CONFIG --- + // --- CONSTANTS --- const SMOOTHNESS = 0.15; - const SETTING_ID = "HALXP-Comfy.Focus.DisplayButton"; - + // Clean IDs + const SETTING_DISPLAY_ID = "HALXP.Focus.DisplayButton"; + const SETTING_AUTO_ID = "HALXP.Focus.AutoEnable"; + + // --- STATE --- let isFocusEnabled = false; + let isWorkflowRunning = false; let activeNode = null; let animationFrameId = null; let focusBtn = null; let dot = null; - // --- 0. REGISTER SETTING --- + // --- 1. REGISTER SETTINGS --- app.ui.settings.addSetting({ - id: SETTING_ID, + id: SETTING_DISPLAY_ID, name: "Display Focus Button", type: "boolean", defaultValue: true, - // MUST be an array. - // Element 0 = Tab Name (HALXP-Comfy) - // Element 1 = Section Header (Focus) - category: ['HALXP-Comfy', 'Focus'], + // 3rd element 'Display' ensures uniqueness + category: ['HALXP-Comfy', 'Focus', 'Display'], tooltip: "Show or hide the HALXP Focus button in the top menu.", onChange: (value) => { - if (value) { - // Try to inject immediately if turned on - injectNextToManager(); - } else { - // Remove button and disable focus if turned off - if (focusBtn) { - focusBtn.remove(); - // Also force stop focus mode if it was active - if (isFocusEnabled) setFocusState(false); - } + if (value) injectNextToManager(); + else { + if (focusBtn) focusBtn.remove(); + if (isFocusEnabled) setFocusState(false); } } }); - // --- 1. CREATE BUTTON --- + app.ui.settings.addSetting({ + id: SETTING_AUTO_ID, + name: "Auto-Focus on Run", + type: "boolean", + defaultValue: true, + // 3rd element 'Auto' ensures uniqueness + category: ['HALXP-Comfy', 'Focus', 'Auto'], + tooltip: "Automatically enable Focus mode when a workflow starts executing.", + }); + + // --- 2. BUTTON LOGIC --- function createButton() { const btn = document.createElement("button"); btn.textContent = "Focus"; btn.id = "halxp-focus-btn"; - - // CSS: Flex-safe, visible, matches Manager style btn.style.cssText = ` - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0 10px; - margin: 0 4px; /* Space between Focus and Manager */ - - background-color: var(--comfy-input-bg, #333); - color: var(--fg-color, #fff); - border: 1px solid var(--border-color, #555); - border-radius: 4px; - - cursor: pointer; - font-family: sans-serif; - font-size: 13px; - font-weight: 600; - - /* CRITICAL VISIBILITY SETTINGS */ - flex-shrink: 0; - white-space: nowrap; - z-index: 9999; - pointer-events: auto; - - /* DEBUG: REMOVE THIS RED BORDER LATER IF YOU WANT */ + position: relative; display: inline-flex; align-items: center; justify-content: center; + padding: 0 10px; margin: 0 4px; background-color: var(--comfy-input-bg, #333); + color: var(--fg-color, #fff); border: 1px solid var(--border-color, #555); + border-radius: 4px; cursor: pointer; font-family: sans-serif; font-size: 13px; + font-weight: 600; flex-shrink: 0; white-space: nowrap; z-index: 9999; pointer-events: auto; border: 1px solid #d3c1c1ff; `; - const d = document.createElement("span"); - d.style.cssText = ` - width: 8px; height: 8px; border-radius: 50%; - background-color: #666; margin-right: 8px; - display: inline-block; - flex-shrink: 0; - `; + d.style.cssText = `width: 8px; height: 8px; border-radius: 50%; background-color: #666; margin-right: 8px; display: inline-block; flex-shrink: 0;`; btn.prepend(d); - - btn.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); - toggleFocus(); - }; - + btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); toggleFocus(); }; dot = d; return btn; } - // --- 2. FIND MANAGER & INJECT --- function injectNextToManager() { - // Check Settings first. If setting is false, do not inject. - const shouldDisplay = app.ui.settings.getSettingValue(SETTING_ID, true); - if (!shouldDisplay) return; - - // Stop if we already exist - if (document.getElementById("halxp-focus-btn")) return; - - // 1. Find the Manager Button - // We look for any button that strictly contains the text "Manager" - const allButtons = Array.from(document.querySelectorAll("button")); + const shouldDisplay = app.ui.settings.getSettingValue(SETTING_DISPLAY_ID, true); + if (!shouldDisplay || document.getElementById("halxp-focus-btn")) return; + const allButtons = Array.from(document.querySelectorAll("button")); const managerBtn = allButtons.find(b => { - // Must be visible if (!b.offsetParent) return false; - const text = b.innerText.trim(); return text === "Manager" || text === "ComfyUI Manager"; }); - // 2. Inject if (managerBtn && managerBtn.parentNode) { - // console.log("[Visual Assist] Found Manager Button. Attaching..."); - if (!focusBtn) focusBtn = createButton(); - - // Match the height of the Manager button exactly const h = managerBtn.offsetHeight; - if (h > 0) focusBtn.style.height = `${h}px`; - else focusBtn.style.height = "28px"; - - // Insert BEFORE the Manager button (to its left) + focusBtn.style.height = h > 0 ? `${h}px` : "28px"; managerBtn.parentNode.insertBefore(focusBtn, managerBtn); - - // Restore state appearance if we were somehow already enabled - if (isFocusEnabled) { - dot.style.backgroundColor = "#4CAF50"; - dot.style.boxShadow = "0 0 6px #4CAF50"; - focusBtn.style.color = "#fff"; - focusBtn.style.borderColor = "#4CAF50"; - } - - return; + if (isFocusEnabled) updateButtonStyle(true); } } - - // --- 3. LOOP --- - // We poll every 500ms because the Manager itself loads slowly. setInterval(injectNextToManager, 500); - - // --- 4. FOCUS LOGIC (Standard) --- - function toggleFocus() { - setFocusState(!isFocusEnabled); - } + // --- 3. FOCUS CORE LOGIC --- + function toggleFocus() { setFocusState(!isFocusEnabled); } function setFocusState(enabled) { isFocusEnabled = enabled; - // Guard: If button doesn't exist (setting off), we can't style it. - if (!focusBtn || !dot) return; + if (focusBtn && dot) updateButtonStyle(enabled); + if (enabled && activeNode) requestAnimationFrame(smoothFollow); + else if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } + } - if (isFocusEnabled) { - dot.style.backgroundColor = "#4CAF50"; - dot.style.boxShadow = "0 0 6px #4CAF50"; - focusBtn.style.color = "#fff"; - focusBtn.style.borderColor = "#4CAF50"; - if (activeNode) requestAnimationFrame(smoothFollow); + function updateButtonStyle(enabled) { + if (!dot || !focusBtn) return; + if (enabled) { + dot.style.backgroundColor = "#4CAF50"; dot.style.boxShadow = "0 0 6px #4CAF50"; + focusBtn.style.color = "#fff"; focusBtn.style.borderColor = "#4CAF50"; } else { - dot.style.backgroundColor = "#666"; - dot.style.boxShadow = "none"; - focusBtn.style.color = "var(--fg-color, #ccc)"; - focusBtn.style.borderColor = "#d3c1c1ff"; // Back to disabled - if (animationFrameId) { - cancelAnimationFrame(animationFrameId); - animationFrameId = null; - } + dot.style.backgroundColor = "#666"; dot.style.boxShadow = "none"; + focusBtn.style.color = "var(--fg-color, #ccc)"; focusBtn.style.borderColor = "#d3c1c1ff"; } } - // Listen for canvas drag + // Disable on manual drag const waitForCanvas = setInterval(() => { if (app.canvas && app.canvas.canvas) { clearInterval(waitForCanvas); @@ -187,13 +119,21 @@ app.registerExtension({ } }, 500); + // --- 4. EXECUTION LOOP --- api.addEventListener("executing", (e) => { if (e.detail) { + if (!isWorkflowRunning) { + isWorkflowRunning = true; + if (app.ui.settings.getSettingValue(SETTING_AUTO_ID, true)) setFocusState(true); + } activeNode = app.graph.getNodeById(e.detail); if (isFocusEnabled && activeNode) { app.canvas.setDirty(true, true); if (!animationFrameId) requestAnimationFrame(smoothFollow); } + } else { + isWorkflowRunning = false; + activeNode = null; } }); @@ -201,14 +141,11 @@ app.registerExtension({ if (isFocusEnabled && activeNode) { const canvas = app.canvas; const ds = canvas.ds; - const screenCenterX = canvas.canvas.clientWidth / 2; - const screenCenterY = canvas.canvas.clientHeight / 2; - const nodeCenterX = activeNode.pos[0] + activeNode.size[0] / 2; - const nodeCenterY = activeNode.pos[1] + activeNode.size[1] / 2; - const targetOffsetX = (screenCenterX / ds.scale) - nodeCenterX; - const targetOffsetY = (screenCenterY / ds.scale) - nodeCenterY; - ds.offset[0] += (targetOffsetX - ds.offset[0]) * SMOOTHNESS; - ds.offset[1] += (targetOffsetY - ds.offset[1]) * SMOOTHNESS; + const center = [canvas.canvas.clientWidth / 2, canvas.canvas.clientHeight / 2]; + const nodeCenter = [activeNode.pos[0] + activeNode.size[0] / 2, activeNode.pos[1] + activeNode.size[1] / 2]; + const target = [(center[0] / ds.scale) - nodeCenter[0], (center[1] / ds.scale) - nodeCenter[1]]; + ds.offset[0] += (target[0] - ds.offset[0]) * SMOOTHNESS; + ds.offset[1] += (target[1] - ds.offset[1]) * SMOOTHNESS; canvas.setDirty(true, true); animationFrameId = requestAnimationFrame(smoothFollow); } diff --git a/js/halxp-runmonitor.js b/js/halxp-runmonitor.js new file mode 100644 index 0000000..b564c43 --- /dev/null +++ b/js/halxp-runmonitor.js @@ -0,0 +1,166 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "Comfy.HALXP.RunMonitor", + async setup() { + console.log("[HALXP-RunMonitor] Initializing..."); + + // --- CONSTANTS --- + const ID_ENABLE = "HALXP.RunMonitor.Enable"; + const ID_SUCCESS = "HALXP.RunMonitor.SuccessPath"; + const ID_ERROR = "HALXP.RunMonitor.ErrorPath"; + + // --- STATE & UI ELEMENTS --- + let monitorBtn = null; + let monitorDot = null; + + // --- 1. CORE LOGIC: SYNC & UPDATE --- + + // Updates the visuals of the button (Green/Gray) based on state + function updateButtonVisuals(enabled) { + if (!monitorBtn || !monitorDot) return; + + if (enabled) { + // ACTIVE STATE (Green) + monitorDot.style.backgroundColor = "#4CAF50"; + monitorDot.style.boxShadow = "0 0 6px #4CAF50"; + monitorBtn.style.color = "#fff"; + monitorBtn.style.borderColor = "#4CAF50"; + monitorBtn.title = "Monitor: ON"; + } else { + // INACTIVE STATE (Gray/Default) + monitorDot.style.backgroundColor = "#666"; + monitorDot.style.boxShadow = "none"; + monitorBtn.style.color = "var(--fg-color, #ccc)"; + monitorBtn.style.borderColor = "#d3c1c1ff"; // Matches Focus default border + monitorBtn.title = "Monitor: OFF"; + } + } + + // Sends config to Python backend AND updates UI + function syncConfigToBackend() { + const enabled = app.ui.settings.getSettingValue(ID_ENABLE, false); + const config = { + enabled: enabled, + success_path: app.ui.settings.getSettingValue(ID_SUCCESS, ""), + error_path: app.ui.settings.getSettingValue(ID_ERROR, "") + }; + + // 1. Update the UI Button immediately + updateButtonVisuals(enabled); + + // 2. Send to Backend + api.fetchApi("/halxp/update_config", { + method: "POST", + body: JSON.stringify(config) + }); + } + + // --- 2. REGISTER SETTINGS --- + + app.ui.settings.addSetting({ + id: ID_ENABLE, + name: "Enable External Monitoring", + type: "boolean", + defaultValue: false, + category: ['HALXP-Comfy', 'Run Monitor', 'Enable'], + tooltip: "Enable to run scripts on workflow status change.", + onChange: syncConfigToBackend // Triggers when changed via Menu OR Button + }); + + app.ui.settings.addSetting({ + id: ID_SUCCESS, + name: "Success Command Path", + type: "text", + defaultValue: "", + category: ['HALXP-Comfy', 'Run Monitor', 'Success'], + tooltip: "Full path to a .bat/.sh to run on success.", + onChange: syncConfigToBackend + }); + + app.ui.settings.addSetting({ + id: ID_ERROR, + name: "Error Command Path", + type: "text", + defaultValue: "", + category: ['HALXP-Comfy', 'Run Monitor', 'Error'], + tooltip: "Full path to a .bat/.sh to run on error.", + onChange: syncConfigToBackend + }); + + // --- 3. UI BUTTON CREATION (Copied from Focus) --- + + function createButton() { + const btn = document.createElement("button"); + btn.textContent = "Monitor"; + btn.id = "halxp-monitor-btn"; + + // EXACT STYLE COPY FROM FOCUS.JS + btn.style.cssText = ` + position: relative; display: inline-flex; align-items: center; justify-content: center; + padding: 0 10px; margin: 0 4px; background-color: var(--comfy-input-bg, #333); + color: var(--fg-color, #fff); border: 1px solid var(--border-color, #555); + border-radius: 4px; cursor: pointer; font-family: sans-serif; font-size: 13px; + font-weight: 600; flex-shrink: 0; white-space: nowrap; z-index: 9999; pointer-events: auto; + border: 1px solid #d3c1c1ff; + `; + + // Create the Status Dot + const d = document.createElement("span"); + d.style.cssText = `width: 8px; height: 8px; border-radius: 50%; background-color: #666; margin-right: 8px; display: inline-block; flex-shrink: 0;`; + + btn.prepend(d); + monitorDot = d; + + // Handle Click: Toggle the setting (which triggers onChange -> syncConfigToBackend) + btn.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + const current = app.ui.settings.getSettingValue(ID_ENABLE, false); + app.ui.settings.setSettingValue(ID_ENABLE, !current); + }; + + return btn; + } + + function injectButton() { + // Prevent duplicates + if (document.getElementById("halxp-monitor-btn")) return; + + // Find the "Manager" button or any existing button to latch onto + const allButtons = Array.from(document.querySelectorAll("button")); + const targetBtn = allButtons.find(b => { + if (!b.offsetParent) return false; + const text = b.innerText.trim(); + // Try to place it near Manager, or Focus if it loaded first + return text === "Manager" || text === "ComfyUI Manager" || text === "Focus"; + }); + + if (targetBtn && targetBtn.parentNode) { + monitorBtn = createButton(); + + // Match height of the neighbor + const h = targetBtn.offsetHeight; + monitorBtn.style.height = h > 0 ? `${h}px` : "28px"; + + // Insert BEFORE the target (Standardizes menu flow) + // If you want it specifically AFTER Focus but BEFORE Manager, this generally works + // because extensions load in order. + targetBtn.parentNode.insertBefore(monitorBtn, targetBtn); + + // Set initial visual state + const isEnabled = app.ui.settings.getSettingValue(ID_ENABLE, false); + updateButtonVisuals(isEnabled); + } + } + + // --- 4. INITIALIZATION --- + + // Attempt to inject button periodically until UI is ready + setInterval(injectButton, 500); + + // Sync backend on load (wait slightly for settings to load) + setTimeout(syncConfigToBackend, 1000); + } +}); \ No newline at end of file