diff --git a/README.md b/README.md index bcaf92d..a9e6ec6 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ Because the Modal queue route can spend noticeable time hashing, syncing, and cr The extension also now renders its own global activity badge in the ComfyUI UI during Modal setup and remote execution. That badge stays visible even when ComfyUI's stock queue state does not fully reflect the pre-queue setup phase. The badge now also prunes orphaned prompt entries when a workflow finishes, so stale orange "setup running" indicators do not linger after the remote work is done. +The badge now distinguishes queue-time graph/resource sync from remote-app wait time: it shows `Syncing graph with Modal` while the queue route is scanning and syncing, then `Waiting for Modal app` once the proxy has started but before the first streamed remote node begins executing. For headless remote execution, the worker also installs a minimal `PromptServer.instance` shim before it imports external custom nodes. That keeps route-registering custom node packs from crashing at import time just because the remote worker is not running ComfyUI's full HTTP server. diff --git a/docs/LEARNINGS.md b/docs/LEARNINGS.md index ad64464..27298ac 100644 --- a/docs/LEARNINGS.md +++ b/docs/LEARNINGS.md @@ -44,6 +44,7 @@ - Some custom-node packs register HTTP routes or prompt handlers at import time through `PromptServer.instance`. A headless remote worker still needs to provide a minimal `PromptServer.instance` shim with `routes`, `app`, `supports`, and no-op send methods, or those packs fail during external custom-node initialization before any workflow execution begins. - For good remote UX, one generic "executing" node state is not enough. The frontend needs separate visual states for "setup", "ready in Modal", "currently executing remotely", "finished for this run", and "workflow fully done", with whole-workflow success clearing the temporary remote states back to the normal idle outline. - The global Modal activity badge needs the same cleanup discipline as the per-node overlays. If a prompt entry survives in badge state after the prompt itself is gone, the UI can get stuck showing an orange "setup running" badge even though the workflow finished; pruning orphaned prompt ids fixes that. +- One orange badge state is not expressive enough. The frontend can use queue-time `modal_status setup` for "Syncing graph with Modal", then switch to a separate waiting state when the proxy begins executing but before the first streamed remote-node event arrives, which makes the remote lifecycle much easier to understand from the UI. - Warm-container caches and loader reuse can conflict with `modal.Volume.reload()`. If a request truly needs a fresh volume reload after uploading new assets, the worker may have to clear warm loader caches, drop reusable prompt executors, unload ComfyUI models, and retry the reload once to get past Modal's "open files preventing the operation" error. - The synthetic frontend events have to match ComfyUI's websocket adapter shapes exactly, not just semantically. In practice that means `status` detail must be the inner status payload, `executing` detail should be the display node id string, and queue-list initialization state benefits from a matching `"Waiting for a machine"` notification payload keyed by prompt id. - Even with correctly-shaped synthetic events, ComfyUI's built-in queue UI may still not present an obvious "workflow running" state during long pre-queue Modal setup. The extension should own a separate global activity badge so users have explicit feedback while the remote runtime is still being prepared. diff --git a/tests/test_web_modal_toggle.py b/tests/test_web_modal_toggle.py index a31a163..7653f0a 100644 --- a/tests/test_web_modal_toggle.py +++ b/tests/test_web_modal_toggle.py @@ -34,7 +34,8 @@ def test_global_modal_status_badge_is_installed() -> None: source = _modal_toggle_source() assert 'element.id = "comfy-modal-global-status";' in source - assert "Modal setup running for" in source + assert "Syncing graph with Modal" in source + assert "Waiting for Modal app" in source assert "Modal workflow running on" in source assert "installGlobalStatusStyles()" in source assert "function pruneGlobalStatusStates()" in source @@ -58,6 +59,7 @@ def test_remote_modal_uses_distinct_ready_active_and_complete_colors() -> None: assert 'const READY_BORDER_COLOR = "#22c55e";' in source assert 'const ACTIVE_BORDER_COLOR = "#a855f7";' in source assert 'const COMPLETE_BORDER_COLOR = "#16a34a";' in source + assert 'const STATE_WAITING = "waiting";' in source assert 'const STATE_READY = "ready";' in source assert 'const STATE_ACTIVE = "active";' in source assert 'detail.phase === "execution_success"' in source diff --git a/web/modal_toggle.js b/web/modal_toggle.js index ae074e7..26cefcf 100644 --- a/web/modal_toggle.js +++ b/web/modal_toggle.js @@ -13,6 +13,7 @@ const COMPLETE_BORDER_COLOR = "#16a34a"; const ERROR_BORDER_COLOR = "#ef4444"; const STATE_SETUP = "setup"; +const STATE_WAITING = "waiting"; const EXECUTION_PHASE = "executing"; const STATE_READY = "ready"; const STATE_ACTIVE = "active"; @@ -155,6 +156,7 @@ function currentGlobalStatus() { return ( phases.find((state) => state.phase === STATE_ERROR) ?? phases.find((state) => state.phase === STATE_SETUP) ?? + phases.find((state) => state.phase === STATE_WAITING) ?? phases.find((state) => state.phase === EXECUTION_PHASE) ?? phases[0] ); @@ -189,7 +191,14 @@ function refreshGlobalStatusElement() { dot.style.background = SETUP_BORDER_COLOR; dot.style.boxShadow = "0 0 0 6px rgba(245, 158, 11, 0.18)"; dot.style.animation = "modal-status-pulse 1.1s ease-in-out infinite"; - text.textContent = `Modal setup running for ${activeState.nodeCount} ${nodeLabel}`; + text.textContent = "Syncing graph with Modal"; + } else if (activeState.phase === STATE_WAITING) { + element.style.borderColor = "rgba(245, 158, 11, 0.55)"; + element.style.background = "rgba(61, 42, 9, 0.94)"; + dot.style.background = SETUP_BORDER_COLOR; + dot.style.boxShadow = "0 0 0 6px rgba(245, 158, 11, 0.18)"; + dot.style.animation = "modal-status-pulse 1.1s ease-in-out infinite"; + text.textContent = "Waiting for Modal app"; } else if (activeState.phase === EXECUTION_PHASE) { element.style.borderColor = "rgba(34, 197, 94, 0.55)"; element.style.background = "rgba(8, 49, 28, 0.94)"; @@ -660,7 +669,11 @@ function handleExecutionPhase(event, phase) { return; } if (phase === EXECUTION_PHASE) { - setGlobalStatusPhase(promptId, EXECUTION_PHASE, componentNodeIds.length); + setGlobalStatusPhase( + promptId, + promptState.hasStreamedProgress ? EXECUTION_PHASE : STATE_WAITING, + componentNodeIds.length, + ); setNodesPhase(componentNodeIds, STATE_READY, promptId, detail.exception_message); return; }