comfyui-mcp 0.48.21 → 0.48.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/comfyui/client.js +46 -6
- package/dist/comfyui/client.js.map +1 -1
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/orchestrator/error-text.js +29 -1
- package/dist/orchestrator/error-text.js.map +1 -1
- package/dist/orchestrator/error-text.test.js +28 -0
- package/dist/orchestrator/error-text.test.js.map +1 -1
- package/dist/orchestrator/index.js +331 -56
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +122 -31
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/orchestrator/panel-tools.js +1256 -198
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/ready-banner.js +70 -0
- package/dist/orchestrator/ready-banner.js.map +1 -0
- package/dist/orchestrator/session-store.js +163 -28
- package/dist/orchestrator/session-store.js.map +1 -1
- package/dist/services/api-nodes.js +5 -7
- package/dist/services/api-nodes.js.map +1 -1
- package/dist/services/download-cache.js +126 -18
- package/dist/services/download-cache.js.map +1 -1
- package/dist/services/download-jobs.js +463 -14
- package/dist/services/download-jobs.js.map +1 -1
- package/dist/services/download-progress.js +252 -3
- package/dist/services/download-progress.js.map +1 -1
- package/dist/services/env-capabilities.js +80 -9
- package/dist/services/env-capabilities.js.map +1 -1
- package/dist/services/generate-audio.js +16 -0
- package/dist/services/generate-audio.js.map +1 -1
- package/dist/services/local-models-fallback.js +7 -3
- package/dist/services/local-models-fallback.js.map +1 -1
- package/dist/services/manifest.js +180 -65
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/model-resolver.js +267 -11
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-management.js +179 -45
- package/dist/services/node-management.js.map +1 -1
- package/dist/services/output-dir.js +33 -3
- package/dist/services/output-dir.js.map +1 -1
- package/dist/services/panel-secrets.js +19 -7
- package/dist/services/panel-secrets.js.map +1 -1
- package/dist/services/process-control.js +107 -8
- package/dist/services/process-control.js.map +1 -1
- package/dist/services/queue-monitor.js +45 -2
- package/dist/services/queue-monitor.js.map +1 -1
- package/dist/services/queue-monitor.stall.test.js +78 -0
- package/dist/services/queue-monitor.stall.test.js.map +1 -0
- package/dist/services/secure-bridge.js +84 -0
- package/dist/services/secure-bridge.js.map +1 -1
- package/dist/services/storage/azure-blob.js +8 -3
- package/dist/services/storage/azure-blob.js.map +1 -1
- package/dist/services/storage/index.js +3 -3
- package/dist/services/storage/index.js.map +1 -1
- package/dist/services/storage/s3.js +9 -3
- package/dist/services/storage/s3.js.map +1 -1
- package/dist/services/ui-bridge.js +341 -24
- package/dist/services/ui-bridge.js.map +1 -1
- package/dist/services/workflow-converter.js +767 -72
- package/dist/services/workflow-converter.js.map +1 -1
- package/dist/services/workflow-deps.js +20 -1
- package/dist/services/workflow-deps.js.map +1 -1
- package/dist/services/workflow-lock.js +127 -27
- package/dist/services/workflow-lock.js.map +1 -1
- package/dist/services/workflow-target-store.js +7 -3
- package/dist/services/workflow-target-store.js.map +1 -1
- package/dist/services/workspace-env.js +48 -0
- package/dist/services/workspace-env.js.map +1 -1
- package/dist/tools/diagnostics.js +10 -1
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/generate-audio.js +44 -0
- package/dist/tools/generate-audio.js.map +1 -1
- package/dist/tools/model-explorer.js +85 -5
- package/dist/tools/model-explorer.js.map +1 -1
- package/dist/tools/model-extras.js +24 -4
- package/dist/tools/model-extras.js.map +1 -1
- package/dist/tools/model-management.js +124 -14
- package/dist/tools/model-management.js.map +1 -1
- package/dist/tools/report-issue.js +220 -66
- package/dist/tools/report-issue.js.map +1 -1
- package/dist/tools/report-issue.test.js +223 -83
- package/dist/tools/report-issue.test.js.map +1 -1
- package/dist/tools/skills-access.js +7 -2
- package/dist/tools/skills-access.js.map +1 -1
- package/dist/tools/template-schema.js +5 -5
- package/dist/tools/template-schema.js.map +1 -1
- package/dist/tools/workflow-compose.js +18 -3
- package/dist/tools/workflow-compose.js.map +1 -1
- package/dist/tools/workflow-visualize.js +1 -1
- package/dist/tools/workflow-visualize.js.map +1 -1
- package/package.json +1 -1
- package/packs/anima/install-runpod.sh +0 -1
- package/packs/anima/install-windows.bat +0 -1
- package/packs/anima/manifest.yaml +0 -1
- package/packs/anima-img2img/install-runpod.sh +0 -1
- package/packs/anima-img2img/install-windows.bat +0 -1
- package/packs/anima-img2img/manifest.yaml +0 -1
- package/packs/anima-inpaint/install-runpod.sh +0 -1
- package/packs/anima-inpaint/install-windows.bat +0 -1
- package/packs/anima-inpaint/manifest.yaml +0 -1
- package/packs/anima-txt2img/install-runpod.sh +0 -1
- package/packs/anima-txt2img/install-windows.bat +0 -1
- package/packs/anima-txt2img/manifest.yaml +0 -1
- package/packs/artokun-flow/install-runpod.sh +0 -1
- package/packs/artokun-flow/install-windows.bat +0 -1
- package/packs/artokun-flow/manifest.yaml +0 -1
- package/packs/ernie/install-runpod.sh +0 -1
- package/packs/ernie/install-windows.bat +0 -1
- package/packs/ernie/manifest.yaml +0 -1
- package/packs/ernie-combo/install-runpod.sh +0 -1
- package/packs/ernie-combo/install-windows.bat +0 -1
- package/packs/ernie-combo/manifest.yaml +0 -1
- package/packs/ernie-img2img/install-runpod.sh +0 -1
- package/packs/ernie-img2img/install-windows.bat +0 -1
- package/packs/ernie-img2img/manifest.yaml +0 -1
- package/packs/ernie-txt2img/install-runpod.sh +0 -1
- package/packs/ernie-txt2img/install-windows.bat +0 -1
- package/packs/ernie-txt2img/manifest.yaml +0 -1
- package/packs/ideogram/install-runpod.sh +0 -1
- package/packs/ideogram/install-windows.bat +0 -1
- package/packs/ideogram/manifest.yaml +0 -1
- package/packs/ideogram-img2img/install-runpod.sh +0 -1
- package/packs/ideogram-img2img/install-windows.bat +0 -1
- package/packs/ideogram-img2img/manifest.yaml +0 -1
- package/packs/ideogram-txt2img/install-runpod.sh +0 -1
- package/packs/ideogram-txt2img/install-windows.bat +0 -1
- package/packs/ideogram-txt2img/manifest.yaml +0 -1
- package/packs/ltx-2.3/install-runpod.sh +0 -1
- package/packs/ltx-2.3/install-windows.bat +0 -1
- package/packs/ltx-2.3/manifest.yaml +0 -1
- package/packs/ltx-2.3-extender/install-runpod.sh +0 -1
- package/packs/ltx-2.3-extender/install-windows.bat +0 -1
- package/packs/ltx-2.3-extender/manifest.yaml +0 -1
- package/packs/ltx-2.3-extender-no-audio/install-runpod.sh +0 -1
- package/packs/ltx-2.3-extender-no-audio/install-windows.bat +0 -1
- package/packs/ltx-2.3-extender-no-audio/manifest.yaml +0 -1
- package/packs/ltx-2.3-flf/install-runpod.sh +0 -1
- package/packs/ltx-2.3-flf/install-windows.bat +0 -1
- package/packs/ltx-2.3-flf/manifest.yaml +0 -1
- package/packs/ltx-2.3-img2vid/install-runpod.sh +0 -1
- package/packs/ltx-2.3-img2vid/install-windows.bat +0 -1
- package/packs/ltx-2.3-img2vid/manifest.yaml +3 -4
- package/packs/ltx-2.3-txt2vid/install-runpod.sh +0 -1
- package/packs/ltx-2.3-txt2vid/install-windows.bat +0 -1
- package/packs/ltx-2.3-txt2vid/manifest.yaml +3 -4
- package/packs/ltx-2.3-xy-plot/install-runpod.sh +0 -1
- package/packs/ltx-2.3-xy-plot/install-windows.bat +0 -1
- package/packs/ltx-2.3-xy-plot/manifest.yaml +0 -1
- package/packs/ltx23-distill-3stage/install-runpod.sh +0 -1
- package/packs/ltx23-distill-3stage/install-windows.bat +0 -1
- package/packs/ltx23-distill-3stage/manifest.yaml +0 -1
- package/packs/qwen-image/install-runpod.sh +0 -1
- package/packs/qwen-image/install-windows.bat +0 -1
- package/packs/qwen-image/manifest.yaml +0 -1
- package/packs/qwen-image-edit/install-runpod.sh +0 -1
- package/packs/qwen-image-edit/install-windows.bat +0 -1
- package/packs/qwen-image-edit/manifest.yaml +0 -1
- package/packs/qwen-image-edit-edit/install-runpod.sh +0 -1
- package/packs/qwen-image-edit-edit/install-windows.bat +0 -1
- package/packs/qwen-image-edit-edit/manifest.yaml +0 -1
- package/packs/wan-animate/install-runpod.sh +0 -1
- package/packs/wan-animate/install-windows.bat +0 -1
- package/packs/wan-animate/manifest.yaml +0 -1
- package/packs/wan-animate-character/install-runpod.sh +0 -1
- package/packs/wan-animate-character/install-windows.bat +0 -1
- package/packs/wan-animate-character/manifest.yaml +0 -1
- package/packs/wan-animate-ofm/install-runpod.sh +0 -1
- package/packs/wan-animate-ofm/install-windows.bat +0 -1
- package/packs/wan-animate-ofm/manifest.yaml +0 -1
- package/packs/wan-longer-videos/install-runpod.sh +0 -1
- package/packs/wan-longer-videos/install-windows.bat +0 -1
- package/packs/wan-longer-videos/manifest.yaml +0 -1
- package/packs/wan-longer-videos-i2v/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-i2v/install-windows.bat +0 -1
- package/packs/wan-longer-videos-i2v/manifest.yaml +0 -1
- package/packs/wan-longer-videos-i2v-96gb/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-i2v-96gb/install-windows.bat +0 -1
- package/packs/wan-longer-videos-i2v-96gb/manifest.yaml +0 -1
- package/packs/wan-longer-videos-t2v/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-t2v/install-windows.bat +0 -1
- package/packs/wan-longer-videos-t2v/manifest.yaml +0 -1
- package/packs/wan-longer-videos-t2v-96gb/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-t2v-96gb/install-windows.bat +0 -1
- package/packs/wan-longer-videos-t2v-96gb/manifest.yaml +0 -1
- package/packs/wan-longer-videos-v2v/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-v2v/install-windows.bat +0 -1
- package/packs/wan-longer-videos-v2v/manifest.yaml +0 -1
- package/packs/wan-longer-videos-v2v-96gb/install-runpod.sh +0 -1
- package/packs/wan-longer-videos-v2v-96gb/install-windows.bat +0 -1
- package/packs/wan-longer-videos-v2v-96gb/manifest.yaml +0 -1
- package/packs/wan-multitalk/install-runpod.sh +0 -1
- package/packs/wan-multitalk/install-windows.bat +0 -1
- package/packs/wan-multitalk/manifest.yaml +0 -1
- package/packs/wan-pusa-extend/install-runpod.sh +0 -1
- package/packs/wan-pusa-extend/install-windows.bat +0 -1
- package/packs/wan-pusa-extend/manifest.yaml +0 -1
- package/packs/wan-transparent/install-runpod.sh +0 -1
- package/packs/wan-transparent/install-windows.bat +0 -1
- package/packs/wan-transparent/manifest.yaml +0 -1
- package/packs/wan-transparent-img2vid/install-runpod.sh +0 -1
- package/packs/wan-transparent-img2vid/install-windows.bat +0 -1
- package/packs/wan-transparent-img2vid/manifest.yaml +0 -1
- package/packs/wan-transparent-img2vid-96gb/install-runpod.sh +0 -1
- package/packs/wan-transparent-img2vid-96gb/install-windows.bat +0 -1
- package/packs/wan-transparent-img2vid-96gb/manifest.yaml +0 -1
- package/packs/z-image-base/install-runpod.sh +0 -1
- package/packs/z-image-base/install-windows.bat +0 -1
- package/packs/z-image-base/manifest.yaml +0 -1
- package/packs/z-image-base-combo/install-runpod.sh +0 -1
- package/packs/z-image-base-combo/install-windows.bat +0 -1
- package/packs/z-image-base-combo/manifest.yaml +0 -1
- package/packs/z-image-base-controlnet/install-runpod.sh +0 -1
- package/packs/z-image-base-controlnet/install-windows.bat +0 -1
- package/packs/z-image-base-controlnet/manifest.yaml +0 -1
- package/packs/z-image-base-img2img/install-runpod.sh +0 -1
- package/packs/z-image-base-img2img/install-windows.bat +0 -1
- package/packs/z-image-base-img2img/manifest.yaml +0 -1
- package/packs/z-image-base-inpaint/install-runpod.sh +0 -1
- package/packs/z-image-base-inpaint/install-windows.bat +0 -1
- package/packs/z-image-base-inpaint/manifest.yaml +0 -1
- package/packs/z-image-base-txt2img/install-runpod.sh +0 -1
- package/packs/z-image-base-txt2img/install-windows.bat +0 -1
- package/packs/z-image-base-txt2img/manifest.yaml +0 -1
- package/packs/z-image-turbo/install-runpod.sh +0 -1
- package/packs/z-image-turbo/install-windows.bat +0 -1
- package/packs/z-image-turbo/manifest.yaml +0 -1
- package/packs/z-image-turbo-combo/install-runpod.sh +0 -1
- package/packs/z-image-turbo-combo/install-windows.bat +0 -1
- package/packs/z-image-turbo-combo/manifest.yaml +0 -1
- package/packs/z-image-turbo-controlnet/install-runpod.sh +0 -1
- package/packs/z-image-turbo-controlnet/install-windows.bat +0 -1
- package/packs/z-image-turbo-controlnet/manifest.yaml +0 -1
- package/packs/z-image-turbo-detail-daemon/install-runpod.sh +0 -1
- package/packs/z-image-turbo-detail-daemon/install-windows.bat +0 -1
- package/packs/z-image-turbo-detail-daemon/manifest.yaml +0 -1
- package/packs/z-image-turbo-img2img/install-runpod.sh +0 -1
- package/packs/z-image-turbo-img2img/install-windows.bat +0 -1
- package/packs/z-image-turbo-img2img/manifest.yaml +0 -1
- package/packs/z-image-turbo-inpainting/install-runpod.sh +0 -1
- package/packs/z-image-turbo-inpainting/install-windows.bat +0 -1
- package/packs/z-image-turbo-inpainting/manifest.yaml +0 -1
- package/packs/z-image-turbo-txt2img/install-runpod.sh +0 -1
- package/packs/z-image-turbo-txt2img/install-windows.bat +0 -1
- package/packs/z-image-turbo-txt2img/manifest.yaml +0 -1
- package/packs/z-image-xy-plot/install-runpod.sh +0 -1
- package/packs/z-image-xy-plot/install-windows.bat +0 -1
- package/packs/z-image-xy-plot/manifest.yaml +0 -1
- package/plugin/skills/flux-txt2img/SKILL.md +9 -4
- package/plugin/skills/wan-scail-replacement/SKILL.md +95 -0
- package/scripts/sync-agents.mjs +18 -8
- package/scripts/test-packs.sh +8 -3
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import { randomBytes } from "node:crypto";
|
|
17
17
|
import readline from "node:readline";
|
|
18
18
|
import { startUiBridge, isLoopbackBindHost } from "../services/ui-bridge.js";
|
|
19
|
-
import { setupSecureBridge } from "../services/secure-bridge.js";
|
|
19
|
+
import { setupSecureBridge, resolveComfyuiPathForTarget } from "../services/secure-bridge.js";
|
|
20
20
|
import { startQuickTunnel } from "../services/tunnel.js";
|
|
21
21
|
import { detectInstallMode } from "../services/self-update.js";
|
|
22
22
|
import { SelfRestarter } from "../services/self-restart.js";
|
|
@@ -26,7 +26,7 @@ import { uploadImageHttp, resetClient } from "../comfyui/client.js";
|
|
|
26
26
|
import { logger } from "../utils/logger.js";
|
|
27
27
|
import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffort, } from "./panel-agent.js";
|
|
28
28
|
import { promptText } from "./error-text.js";
|
|
29
|
-
import { createPanelMcpServer } from "./panel-tools.js";
|
|
29
|
+
import { createPanelMcpServer, makePanelToolCtx, resolvePinTarget } from "./panel-tools.js";
|
|
30
30
|
import { optionsAckFrame, optionsErrorAckFrame, optionsRequestMeta, } from "./options-ack.js";
|
|
31
31
|
import { readUserMcpServers } from "../services/user-mcp-config.js";
|
|
32
32
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -50,6 +50,7 @@ import { resolvePrompt, registerPrompt, onPromptsChanged } from "../services/pro
|
|
|
50
50
|
import { allBackendReadiness } from "./backend-readiness.js";
|
|
51
51
|
import { handleOAuthBegin, handleOAuthStatus, handleOAuthSignout } from "./oauth-bridge.js";
|
|
52
52
|
import { buildStartFailureNotice } from "./start-failure-notice.js";
|
|
53
|
+
import { readyBannerText, bannerCorrection } from "./ready-banner.js";
|
|
53
54
|
import { OAUTH_PROVIDERS } from "../services/oauth-flow.js";
|
|
54
55
|
import { startPanelMcpHttpServer } from "./panel-mcp-http.js";
|
|
55
56
|
import { startPanelConsoleHttpServer } from "./panel-console-http.js";
|
|
@@ -64,7 +65,7 @@ import { unloadAllOllama, warmOllama, resolveOllamaHost } from "../services/olla
|
|
|
64
65
|
import { isLocalLmstudio, startLmstudioServer, unloadAllLmstudio, warmLmstudio, } from "../services/lmstudio-lifecycle.js";
|
|
65
66
|
import { llamacppProps, llamacppToolsReady } from "../services/llamacpp-probe.js";
|
|
66
67
|
import { getAgentSettings, setAgentSettings, normalizePreferredModels } from "../services/panel-settings.js";
|
|
67
|
-
import { gatherEnvCapabilities, buildPanelSystemAppend, } from "../services/env-capabilities.js";
|
|
68
|
+
import { gatherEnvCapabilities, buildPanelSystemAppend, resolveBackends, } from "../services/env-capabilities.js";
|
|
68
69
|
import { WorkflowTargetStore } from "../services/workflow-target-store.js";
|
|
69
70
|
const PANEL_SYSTEM_APPEND = `You are the autonomous assistant embedded directly in a ComfyUI sidebar panel. The person is working in ComfyUI and talks to you through that panel: their messages arrive as your prompts, and everything you write is shown to them in the panel chat. Write for that reader — lead with the result, keep replies short and concrete, and don't narrate routine internal steps.
|
|
70
71
|
|
|
@@ -80,11 +81,11 @@ If a workflow needs a custom node the user doesn't have, don't silently skip it
|
|
|
80
81
|
|
|
81
82
|
CRASH RECOVERY — when a custom node BREAKS or CRASHED ComfyUI, fix it before giving up. If your turn begins with a "⚠️ ComfyUI crashed …" note (it names the fatal log block and the most likely culprit custom node + file:line), or a run dies with a node-level error you can pin to one pack, do NOT just re-run the same graph — ESCALATE to actually fix that node, narrating each step to the user as you go: (a) UPDATE it to the latest code — call panel_update_node with the culprit's id (or the comfyui MCP update_custom_node / fix_custom_node). Try version 'nightly' to grab a just-landed upstream fix. Poll panel_node_queue_status, then panel_restart_comfyui → you resume and RETRY the action to see if the crash is gone. (b) If updating doesn't fix it, reach into COMFYUI_PATH/custom_nodes/<NodeDir> with your shell (Bash): if it's a git repo (a .git dir), run git fetch && git pull (or check out the nightly branch) to force the latest, reinstall its requirements if needed, then restart + retry. (c) If there's no git or it's still broken, attempt a TARGETED source patch of the crashing file:line, then VERIFY the fix actually resolves the crash (restart + retry the same action — confirm it no longer faults). Once verified, OFFER to suggest the fix upstream to the repo owner (open an issue or PR describing the crash + your patch) — describe it and ask the user first; do NOT auto-file anything. Combine this cleanly with the normal install→restart→continue flow above: a fresh install that crashes on first use is the same loop (update/patch the just-installed node, don't abandon it).
|
|
82
83
|
|
|
83
|
-
REPORT OUR OWN BUGS (we're in beta — bias HARD toward filing) — distinct from the third-party case above. Whenever YOU hit a defect in comfyui-mcp or the sidebar panel ITSELF — a tool/panel/orchestrator call that errors, crashes ComfyUI, returns wrong/misleading/empty output, fails silently, or a limitation that forced you into an awkward workaround/retry — invoke the report-bug skill (read_skill "report-bug") to autonomously file a via-panel report, EVEN IF you already recovered or worked around it. You do NOT need to be blocked, and it does NOT need to be fatal — the workaround itself is the signal. The skill scrubs secrets and files through our intake Worker (which dedupes server-side, so over-reporting is cheap); then just drop the user a one-line note. Do NOT ask permission for OUR bugs — file them autonomously (unlike third-party/custom-node bugs above, where you OFFER and ask first). Ordinary generation errors (OOM, missing model, bad params, user mistakes) are NOT bug reports — those stay troubleshooting.
|
|
84
|
+
REPORT OUR OWN BUGS (we're in beta — bias HARD toward filing) — distinct from the third-party case above. Whenever YOU hit a defect in comfyui-mcp or the sidebar panel ITSELF — a tool/panel/orchestrator call that errors, crashes ComfyUI, returns wrong/misleading/empty output, fails silently, or a limitation that forced you into an awkward workaround/retry — invoke the report-bug skill (read_skill "report-bug") to autonomously file a via-panel report, EVEN IF you already recovered or worked around it. You do NOT need to be blocked, and it does NOT need to be fatal — the workaround itself is the signal. AND report the FIX, not just the failure: any time a defect in comfyui-mcp or the panel gets fixed or patched during the session — you edited/worked around the code, changed a setting, or otherwise made a broken thing work — and the user confirms it's working (they're happy with the fix), file the report RIGHT THEN, capturing BOTH the original bug AND the fix that worked, so it can be fixed upstream at the source (a confirmed working fix is the most valuable thing to send upstream). File it immediately and autonomously — do NOT wait, and do NOT ask. The skill scrubs secrets and files through our intake Worker (which dedupes server-side, so over-reporting is cheap); then just drop the user a one-line note. Do NOT ask permission for OUR bugs — file them autonomously (unlike third-party/custom-node bugs above, where you OFFER and ask first). Ordinary generation errors (OOM, missing model, bad params, user mistakes) are NOT bug reports — those stay troubleshooting.
|
|
84
85
|
|
|
85
86
|
WEDGED RENDER / OOM / VRAM PINNED — when a generation is stuck or hits CUDA out-of-memory, or a cancel didn't actually free GPU memory (models still resident, VRAM pinned, the next run still OOMs), call panel_free_vram to UNLOAD all models and free VRAM before retrying — it does NOT restart ComfyUI, so it's the cheap first move. Escalation ladder: cancel the run → panel_free_vram (unload + free) → retry; only as a LAST RESORT panel_restart_comfyui (which refuses mid-render and guards the running generation). Reach for panel_free_vram before a restart whenever a cancel left memory pinned.
|
|
86
87
|
|
|
87
|
-
WORKFLOW TARGETING — by default your panel_* graph edits follow whichever workflow tab the user is currently viewing.
|
|
88
|
+
WORKFLOW TARGETING — by default your panel_* graph edits follow whichever workflow tab the user is currently viewing. The panel can only read or edit the workflow currently IN VIEW, so to work on a specific open workflow, make it the active canvas first with panel_open_workflow, then call panel_set_workflow_target(mode:"pinned", path:<from panel_list_workflows>) to bind your edits to it; panel_get_workflow_target shows the current binding. Pinning to a background (open but not active) workflow is REJECTED at pin time — it cannot route edits to a tab that isn't in view. A pin does NOT switch what the user sees; it binds your edits to that workflow so that if the user later switches away, your next graph call fails loudly instead of silently editing the wrong graph. Set mode:"current" to follow the user's active tab again.
|
|
88
89
|
|
|
89
90
|
CRITICAL — never destroy the user's work. When they ask for a "new workflow", a "fresh canvas", or to "start over for a new project", call panel_new_workflow (it opens a NEW TAB and leaves their current workflow intact). NEVER use panel_clear for that — panel_clear wipes the CURRENTLY OPEN graph and is ONLY for an explicit "clear/reset this canvas". You can manage tabs with panel_list_workflows / panel_open_workflow / panel_rename_workflow / panel_close_workflow, and group nodes with panel_select_nodes / panel_create_subgraph. To label a node by its purpose, use panel_set_node_title. To read or edit nodes INSIDE a subgraph, call panel_enter_subgraph(node_id) first — then panel_query_graph / panel_graph_outline and the panel_* edit tools operate on the subgraph's inner nodes — and panel_exit_subgraph when you're done.
|
|
90
91
|
|
|
@@ -94,7 +95,7 @@ MERGE / COMPOSE WORKFLOWS — to bring nodes from ONE workflow into ANOTHER (com
|
|
|
94
95
|
|
|
95
96
|
REUSE SUBGRAPHS via the blueprint library — when the user builds a useful subgraph and wants to reuse it (now or in other workflows), SAVE it: panel_create_subgraph to group the nodes (if not already a subgraph), then panel_save_subgraph(node_id, name) publishes it to their library programmatically (no dialog). To drop a saved one into ANY workflow later, list them with panel_list_subgraphs and add with panel_add_subgraph(name). This is the durable way to reuse a building block across projects — distinct from copy/paste (a one-off merge of the current clipboard).
|
|
96
97
|
|
|
97
|
-
PREFER READY EXPERTISE OVER HAND-BUILDING. When the user asks you to "set up", "build", or "make" a workflow for a specific model FAMILY (krea2, wan, flux, qwen, ltx, z-image, ideogram, anima, ernie, etc.), do NOT immediately hand-build a generic graph from scratch. FIRST, in order: (a) consult the matching SKILL for that family. If you already have a skill for it loaded in your context, use it. If you do NOT have its full guidance in front of you, do NOT guess from memory — actually CALL the comfyui MCP's list_skills to see what's bundled, then read_skill(name) to load the real family expertise (model slots, the node graph, settings, gotchas) before you build; (b) check the installer PACKS by CALLING list_packs (each packs/<name>/ has a ready manifest.yaml AND a ready workflow.json). If a pack matches the family, PREFER it: apply_manifest --path <its manifest_path> installs the right custom nodes + model weights, and the pack's workflow.json is the expert graph — CALL read_pack_workflow(name) to get that ready graph and recreate it on the live canvas via panel_add_node/panel_connect/panel_set_widget so the user watches it build (or enqueue it headlessly when they don't need it on-canvas), instead of inventing your own. Don't claim a skill or pack exists unless a tool result confirmed it; (c) check the
|
|
98
|
+
PREFER READY EXPERTISE OVER HAND-BUILDING. When the user asks you to "set up", "build", or "make" a workflow for a specific model FAMILY (krea2, wan, flux, qwen, ltx, z-image, ideogram, anima, ernie, etc.), do NOT immediately hand-build a generic graph from scratch. FIRST, in order: (a) consult the matching SKILL for that family. If you already have a skill for it loaded in your context, use it. If you do NOT have its full guidance in front of you, do NOT guess from memory — actually CALL the comfyui MCP's list_skills to see what's bundled, then read_skill(name) to load the real family expertise (model slots, the node graph, settings, gotchas) before you build; (b) check the installer PACKS by CALLING list_packs (each packs/<name>/ has a ready manifest.yaml AND a ready workflow.json). If a pack matches the family, PREFER it: apply_manifest --path <its manifest_path> installs the right custom nodes + model weights, and the pack's workflow.json is the expert graph — CALL read_pack_workflow(name) to get that ready graph and recreate it on the live canvas via panel_add_node/panel_connect/panel_set_widget so the user watches it build (or enqueue it headlessly when they don't need it on-canvas), instead of inventing your own. Don't claim a skill or pack exists unless a tool result confirmed it; (c) check the ComfyUI workflow Templates — call list_workflow_templates (it lists CUSTOM-NODE-contributed templates from the server's /api/workflow_templates index; it does NOT enumerate ComfyUI's own core bundled templates, which are served separately) for a matching starter, and ALSO point the user at the frontend's Templates browser directly, since core templates only appear there. Only build from scratch if NOTHING matches — and when you do, briefly say what you checked (skill, packs, templates) so the user knows you didn't reinvent the wheel. And never wipe the user's current canvas (no panel_clear) until the replacement is actually ready to drop in. To load a ready pack graph onto the live canvas in one shot (instead of recreating it node-by-node), use panel_load_workflow(pack:<name>) — the pack's UI workflow.json is read server-side and dropped onto the canvas, undoable.
|
|
98
99
|
|
|
99
100
|
OPENING A STAGED / DOWNLOADED WORKFLOW. When you've saved or downloaded a workflow .json into the user's ComfyUI workflows folder (e.g. an example you fetched), open it with panel_open_workflow(path:<name-or-path>) — it now REFRESHES the frontend's (cached) workflow list before searching, so a just-staged file is found and opened natively in its own tab. For a workflow .json that lives OUTSIDE the workflows folder (any absolute path on the ComfyUI machine, or a downloaded example you didn't move into workflows/), load it directly onto the live canvas with panel_load_workflow(path:<file>) — the orchestrator reads + parses the JSON server-side and drops it on the canvas in one shot, so even a large (100KB+) workflow never has to shuttle through this chat. Prefer panel_load_workflow(path:<file>) over pasting a big workflow JSON inline as the graph arg.
|
|
100
101
|
|
|
@@ -965,7 +966,26 @@ export async function runPanelOrchestrator() {
|
|
|
965
966
|
// local install — the spawn env builders prefer COMFYUI_PATH over the
|
|
966
967
|
// force-remote flag, so a leaked path would silently defeat --force-remote.
|
|
967
968
|
const localPathForTarget = (url) => !isForceRemoteFlagSet() && isLoopbackUrl(url) ? localComfyuiPath : undefined;
|
|
968
|
-
|
|
969
|
+
// #296: an embedded LOCAL sidebar session with NO CLI-configured workspace
|
|
970
|
+
// (COMFYUI_PATH unset AND auto-detect empty) would otherwise spawn path-less —
|
|
971
|
+
// its comfyui MCP stuck in a degraded "local but path-less" state (no
|
|
972
|
+
// download_model/apply_manifest/model-scan LOCAL surface, panel_load_workflow's
|
|
973
|
+
// local fallback blind). The panel already KNOWS where ComfyUI lives and serves
|
|
974
|
+
// it at GET /comfyui_mcp_panel/status (base_path). resolveComfyuiPathForTarget
|
|
975
|
+
// consumes it as a last-resort fallback for a loopback, non-force-remote target
|
|
976
|
+
// (adopting only a path that actually exists on THIS machine). Awaited here so
|
|
977
|
+
// the recovered path is in place BEFORE the MCP-env builders / manager below
|
|
978
|
+
// capture it. Best-effort — a failed/absent status route leaves us path-less.
|
|
979
|
+
let comfyuiPath = await resolveComfyuiPathForTarget({
|
|
980
|
+
target: comfyuiUrl,
|
|
981
|
+
localPath: localComfyuiPath,
|
|
982
|
+
forceRemote: isForceRemoteFlagSet(),
|
|
983
|
+
isLoopback: isLoopbackUrl(comfyuiUrl),
|
|
984
|
+
});
|
|
985
|
+
if (comfyuiPath && comfyuiPath !== localPathForTarget(comfyuiUrl)) {
|
|
986
|
+
logger.info(`[panel-orchestrator] no COMFYUI_PATH/default workspace; adopted ComfyUI ` +
|
|
987
|
+
`base_path from the panel status route: ${comfyuiPath} (#296).`);
|
|
988
|
+
}
|
|
969
989
|
// Force the child remote only when opted in (--force-remote) or the target is
|
|
970
990
|
// non-loopback; a default loopback panel user with no COMFYUI_PATH is left to
|
|
971
991
|
// auto-detect its local install (keeps download_model/apply_manifest/scans).
|
|
@@ -1214,8 +1234,30 @@ export async function runPanelOrchestrator() {
|
|
|
1214
1234
|
const defaultBackend = KNOWN_BACKENDS.has(backendId) ? backendId : "claude";
|
|
1215
1235
|
const AGENT_KEY_SEP = "::";
|
|
1216
1236
|
const tabBackends = new Map(); // panel tabId -> selected backend
|
|
1237
|
+
// #376: the model label advertised in a tab's ready banner (sent at hello, before
|
|
1238
|
+
// the SDK reports the real model). onSession re-sends a corrected banner when the
|
|
1239
|
+
// resolved model differs from what's stored here. A tab with NO entry never got a
|
|
1240
|
+
// greeting (a resume/reconnect), so it must never be "corrected".
|
|
1241
|
+
const advertisedBannerModel = new Map();
|
|
1242
|
+
// The SDK-resolved model per tab, learned from the session/init event. Kept so a
|
|
1243
|
+
// greeting emitted AFTER the model is already known (the onSession-before-greeting
|
|
1244
|
+
// race) can label itself correctly instead of showing the pre-init default.
|
|
1245
|
+
const resolvedModelByTab = new Map();
|
|
1217
1246
|
const headlessTabs = new Set(); // tabs with no ComfyUI canvas (mobile/remote) — deliver renders in-turn
|
|
1247
|
+
// #570: for an UNSAVED workflow the panel tab id is an ephemeral tmp:<uuid>,
|
|
1248
|
+
// regenerated on every reload, so the tab-keyed session store can't survive an
|
|
1249
|
+
// orchestrator restart that also reloads the panel. Map each such tab to a STABLE
|
|
1250
|
+
// resume key (origin + title + backend) computed at hello, so onSession can also
|
|
1251
|
+
// persist under it and a reloaded tab can resume it. tmp: tabs only.
|
|
1252
|
+
const tabStableKey = new Map();
|
|
1218
1253
|
const workflowTargets = new WorkflowTargetStore();
|
|
1254
|
+
// Monotonic per-tab sequence for set_workflow_target events. A pinned target is
|
|
1255
|
+
// validated asynchronously (resolvePinTarget queries workflow_list), so a later event
|
|
1256
|
+
// (another pin, or a synchronous mode:"current") can arrive before the async pin
|
|
1257
|
+
// commits. Each event bumps the tab's sequence; a pinned resolution only commits/acks if
|
|
1258
|
+
// its captured sequence is still the latest — otherwise a stale pin would clobber the
|
|
1259
|
+
// user's newer selection (codex race).
|
|
1260
|
+
const workflowTargetSeq = new Map();
|
|
1219
1261
|
const backendForTab = (panelTabId) => tabBackends.get(panelTabId) ?? defaultBackend;
|
|
1220
1262
|
const agentKeyFor = (panelTabId) => panelTabId + AGENT_KEY_SEP + backendForTab(panelTabId);
|
|
1221
1263
|
// A panel tab id never contains "::"; backend names never do — so split on the
|
|
@@ -1272,11 +1314,32 @@ export async function runPanelOrchestrator() {
|
|
|
1272
1314
|
if (gen !== envRefreshGen)
|
|
1273
1315
|
return; // superseded — let the newer refresh own the prompt
|
|
1274
1316
|
// Belt-and-suspenders: gather is internally guarded, but never let a stray
|
|
1275
|
-
// throw break the prompt — fall back to the static append.
|
|
1317
|
+
// throw break the prompt — fall back to the static append. Also DROP any
|
|
1318
|
+
// previously-gathered envCaps so systemAppendForBackend() falls back to the
|
|
1319
|
+
// same static append for EVERY backend (not just the default) — otherwise a
|
|
1320
|
+
// non-default backend would rebuild a stale env block off the old caps,
|
|
1321
|
+
// disagreeing with the reset panelSystemAppend (#358 wiring).
|
|
1322
|
+
envCaps = undefined;
|
|
1276
1323
|
panelSystemAppend = resolvePrompt("panel.persona", PANEL_SYSTEM_APPEND);
|
|
1277
1324
|
logger.debug(`[panel-orchestrator] env-capabilities probe failed (using static prompt): ${err instanceof Error ? err.message : String(err)}`);
|
|
1278
1325
|
}
|
|
1279
1326
|
}
|
|
1327
|
+
// The ENVIRONMENT block's `Backend:` line must name the provider running THIS
|
|
1328
|
+
// tab's turn, not the process default (#358). Backends are per-tab now, so the
|
|
1329
|
+
// single `panelSystemAppend` (built with the default backend id) would tell a
|
|
1330
|
+
// Grok tab it's Claude. Rebuild the block per-tab-backend: same env facts, but
|
|
1331
|
+
// the backend label recomputed from the tab's actual backend id. Falls back to
|
|
1332
|
+
// the shared append when the env probe produced nothing (envCaps undefined).
|
|
1333
|
+
const systemAppendForBackend = (bId) => {
|
|
1334
|
+
if (!envCaps)
|
|
1335
|
+
return panelSystemAppend;
|
|
1336
|
+
const { backend, otherBackendAvailable } = resolveBackends(bId);
|
|
1337
|
+
// Already the default's label → reuse the shared string (no rebuild).
|
|
1338
|
+
if (envCaps.backend === backend && envCaps.otherBackendAvailable === otherBackendAvailable) {
|
|
1339
|
+
return panelSystemAppend;
|
|
1340
|
+
}
|
|
1341
|
+
return buildPanelSystemAppend(resolvePrompt("panel.persona", PANEL_SYSTEM_APPEND), { ...envCaps, backend, otherBackendAvailable });
|
|
1342
|
+
};
|
|
1280
1343
|
// Build it before any agent could spawn. Guarded so a probe stall can't block
|
|
1281
1344
|
// orchestrator startup beyond the probes' own (short) timeouts.
|
|
1282
1345
|
await refreshEnvCapabilities();
|
|
@@ -1425,12 +1488,14 @@ export async function runPanelOrchestrator() {
|
|
|
1425
1488
|
const makeBackend = (key) => {
|
|
1426
1489
|
const backend = backendOf(key);
|
|
1427
1490
|
const panelTabId = panelTabOf(key);
|
|
1491
|
+
// The ENVIRONMENT block's `Backend:` line must name THIS backend (#358).
|
|
1492
|
+
const sysAppend = systemAppendForBackend(backend);
|
|
1428
1493
|
try {
|
|
1429
1494
|
if (backend === "codex") {
|
|
1430
1495
|
return new CodexBackend({
|
|
1431
1496
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1432
1497
|
model: codexModel,
|
|
1433
|
-
systemAppend:
|
|
1498
|
+
systemAppend: sysAppend,
|
|
1434
1499
|
comfyuiUrl,
|
|
1435
1500
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1436
1501
|
});
|
|
@@ -1439,7 +1504,7 @@ export async function runPanelOrchestrator() {
|
|
|
1439
1504
|
return new GeminiBackend({
|
|
1440
1505
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1441
1506
|
model: geminiModel,
|
|
1442
|
-
systemAppend:
|
|
1507
|
+
systemAppend: sysAppend,
|
|
1443
1508
|
comfyuiUrl,
|
|
1444
1509
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1445
1510
|
});
|
|
@@ -1448,7 +1513,7 @@ export async function runPanelOrchestrator() {
|
|
|
1448
1513
|
return new AntigravityBackend({
|
|
1449
1514
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1450
1515
|
...(antigravityModel ? { model: antigravityModel } : {}),
|
|
1451
|
-
systemAppend:
|
|
1516
|
+
systemAppend: sysAppend,
|
|
1452
1517
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1453
1518
|
});
|
|
1454
1519
|
}
|
|
@@ -1456,7 +1521,7 @@ export async function runPanelOrchestrator() {
|
|
|
1456
1521
|
return new GrokBackend({
|
|
1457
1522
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1458
1523
|
model: grokModel,
|
|
1459
|
-
systemAppend:
|
|
1524
|
+
systemAppend: sysAppend,
|
|
1460
1525
|
comfyuiUrl,
|
|
1461
1526
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1462
1527
|
});
|
|
@@ -1465,7 +1530,7 @@ export async function runPanelOrchestrator() {
|
|
|
1465
1530
|
return new OllamaBackend({
|
|
1466
1531
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1467
1532
|
model: ollamaModel,
|
|
1468
|
-
systemAppend:
|
|
1533
|
+
systemAppend: sysAppend,
|
|
1469
1534
|
comfyuiUrl,
|
|
1470
1535
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1471
1536
|
...ollamaDeps(),
|
|
@@ -1475,7 +1540,7 @@ export async function runPanelOrchestrator() {
|
|
|
1475
1540
|
return new OllamaBackend({
|
|
1476
1541
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1477
1542
|
model: openrouterModel,
|
|
1478
|
-
systemAppend:
|
|
1543
|
+
systemAppend: sysAppend,
|
|
1479
1544
|
comfyuiUrl,
|
|
1480
1545
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1481
1546
|
...openrouterDeps(),
|
|
@@ -1485,7 +1550,7 @@ export async function runPanelOrchestrator() {
|
|
|
1485
1550
|
return new OllamaBackend({
|
|
1486
1551
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1487
1552
|
model: lmstudioModel,
|
|
1488
|
-
systemAppend:
|
|
1553
|
+
systemAppend: sysAppend,
|
|
1489
1554
|
comfyuiUrl,
|
|
1490
1555
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1491
1556
|
...lmstudioDeps(),
|
|
@@ -1495,7 +1560,7 @@ export async function runPanelOrchestrator() {
|
|
|
1495
1560
|
return new OllamaBackend({
|
|
1496
1561
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1497
1562
|
model: llamacppModel,
|
|
1498
|
-
systemAppend:
|
|
1563
|
+
systemAppend: sysAppend,
|
|
1499
1564
|
comfyuiUrl,
|
|
1500
1565
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1501
1566
|
...llamacppDeps(),
|
|
@@ -1505,7 +1570,7 @@ export async function runPanelOrchestrator() {
|
|
|
1505
1570
|
return new OllamaBackend({
|
|
1506
1571
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1507
1572
|
model: customModel,
|
|
1508
|
-
systemAppend:
|
|
1573
|
+
systemAppend: sysAppend,
|
|
1509
1574
|
comfyuiUrl,
|
|
1510
1575
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1511
1576
|
...customDeps(),
|
|
@@ -1515,7 +1580,7 @@ export async function runPanelOrchestrator() {
|
|
|
1515
1580
|
return new ChatGptOAuthBackend({
|
|
1516
1581
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1517
1582
|
model: chatgptModel,
|
|
1518
|
-
systemAppend:
|
|
1583
|
+
systemAppend: sysAppend,
|
|
1519
1584
|
comfyuiUrl,
|
|
1520
1585
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1521
1586
|
});
|
|
@@ -1524,7 +1589,7 @@ export async function runPanelOrchestrator() {
|
|
|
1524
1589
|
if (simpleKeyReg) {
|
|
1525
1590
|
// glm/moonshot (and any future simple api-key provider) share one factory.
|
|
1526
1591
|
return makeOpenAiKeyBackend(simpleKeyReg, {
|
|
1527
|
-
systemAppend:
|
|
1592
|
+
systemAppend: sysAppend,
|
|
1528
1593
|
comfyuiUrl,
|
|
1529
1594
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1530
1595
|
});
|
|
@@ -1533,7 +1598,7 @@ export async function runPanelOrchestrator() {
|
|
|
1533
1598
|
return new KimiBackend({
|
|
1534
1599
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1535
1600
|
model: kimiModel,
|
|
1536
|
-
systemAppend:
|
|
1601
|
+
systemAppend: sysAppend,
|
|
1537
1602
|
comfyuiUrl,
|
|
1538
1603
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1539
1604
|
});
|
|
@@ -1546,7 +1611,7 @@ export async function runPanelOrchestrator() {
|
|
|
1546
1611
|
return new CopilotBackend({
|
|
1547
1612
|
cwd: comfyuiPath ?? process.cwd(),
|
|
1548
1613
|
model: copilotModel,
|
|
1549
|
-
systemAppend:
|
|
1614
|
+
systemAppend: sysAppend,
|
|
1550
1615
|
comfyuiUrl,
|
|
1551
1616
|
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
1552
1617
|
});
|
|
@@ -1635,6 +1700,10 @@ export async function runPanelOrchestrator() {
|
|
|
1635
1700
|
COMFYUI_URL: comfyuiUrl,
|
|
1636
1701
|
// Where download_model writes live progress for the panel tray.
|
|
1637
1702
|
COMFYUI_MCP_PROGRESS_DIR: progressDir,
|
|
1703
|
+
// Self-scope this tab's downloads so the orchestrator can wake EXACTLY
|
|
1704
|
+
// this tab's agent when a download settles (#547) — the child stamps its
|
|
1705
|
+
// own COMFYUI_MCP_TAB into each progress row, mirroring COMFYUI_MCP_BLIND.
|
|
1706
|
+
...(panelTab ? { COMFYUI_MCP_TAB: panelTab } : {}),
|
|
1638
1707
|
// Local mode → enables download_model, apply_manifest (installer packs),
|
|
1639
1708
|
// and model scans so the agent installs the right way instead of curl.
|
|
1640
1709
|
...(comfyuiPath ? { COMFYUI_PATH: comfyuiPath } : forceRemoteEnv()),
|
|
@@ -1649,6 +1718,10 @@ export async function runPanelOrchestrator() {
|
|
|
1649
1718
|
makeBackend,
|
|
1650
1719
|
comfyuiUrl, // for fetching image bytes to inline into agent turns
|
|
1651
1720
|
systemAppend: panelSystemAppend,
|
|
1721
|
+
// Per-KEY env block so the CLAUDE path (makeBackend returns undefined) still
|
|
1722
|
+
// gets the tab's actual backend on its `Backend:` line (#358). Non-claude
|
|
1723
|
+
// tabs get it via makeBackend's sysAppend; this covers the built-in default.
|
|
1724
|
+
makeSystemAppend: (key) => systemAppendForBackend(backendOf(key)),
|
|
1652
1725
|
pluginPath: pluginAvailable ? pluginPath : undefined,
|
|
1653
1726
|
// In-process live-graph MCP for CLAUDE keys only (codex/gemini drive the
|
|
1654
1727
|
// canvas through the loopback HTTP MCP instead). Bound to the PANEL tab so
|
|
@@ -1675,9 +1748,34 @@ export async function runPanelOrchestrator() {
|
|
|
1675
1748
|
// Per-response usage → the panel's context/usage meter (updates live).
|
|
1676
1749
|
onStatus: (key, status) => pushStatus(panelTabOf(key), status),
|
|
1677
1750
|
// Report the SDK session id so the panel can persist it and resume on reload.
|
|
1678
|
-
onSession: (key, sessionId) => {
|
|
1679
|
-
|
|
1751
|
+
onSession: (key, sessionId, model) => {
|
|
1752
|
+
const panelTab = panelTabOf(key);
|
|
1753
|
+
// #570: also persist under the tab's STABLE resume key (unsaved workflows),
|
|
1754
|
+
// so a panel reload that regenerates the tmp:<uuid> can still resume this
|
|
1755
|
+
// conversation. The manager already persisted under the exact tab key.
|
|
1756
|
+
const skey = tabStableKey.get(panelTab);
|
|
1757
|
+
if (skey)
|
|
1758
|
+
sessionStore.setStable(skey, sessionId, panelTab);
|
|
1759
|
+
bridge.push({ type: "session", session_id: sessionId }, panelTab);
|
|
1680
1760
|
bridge.broadcastTabList(); // a session started/changed → refresh mirror pickers
|
|
1761
|
+
// #376: the ready banner was sent at hello with the PRE-init default model.
|
|
1762
|
+
// Now the SDK reports the ACTUALLY-resolved model — remember it, then re-send
|
|
1763
|
+
// a corrected banner IFF a banner was actually advertised for this tab AND the
|
|
1764
|
+
// resolved model differs (bannerCorrection returns null otherwise, so a resume
|
|
1765
|
+
// with no prior greeting is never "corrected" and a correct banner never
|
|
1766
|
+
// duplicates).
|
|
1767
|
+
if (typeof model === "string" && model.trim())
|
|
1768
|
+
resolvedModelByTab.set(panelTab, model);
|
|
1769
|
+
const corrected = bannerCorrection({
|
|
1770
|
+
backend: backendForTab(panelTab),
|
|
1771
|
+
advertisedLabel: advertisedBannerModel.get(panelTab),
|
|
1772
|
+
resolvedModel: model,
|
|
1773
|
+
customBaseUrl,
|
|
1774
|
+
});
|
|
1775
|
+
if (corrected) {
|
|
1776
|
+
advertisedBannerModel.set(panelTab, model);
|
|
1777
|
+
bridge.push({ type: "say", text: corrected }, panelTab);
|
|
1778
|
+
}
|
|
1681
1779
|
},
|
|
1682
1780
|
// Per-turn rewind anchor (assistant UUID) → the panel stores it so a later
|
|
1683
1781
|
// "rewind conversation to here" can fork the session at that point.
|
|
@@ -1856,6 +1954,38 @@ export async function runPanelOrchestrator() {
|
|
|
1856
1954
|
// (hello, runpod tools, watcher auto-stop/vanish), so they can't drift
|
|
1857
1955
|
// split-brained again (#269).
|
|
1858
1956
|
logger.info(`[panel-orchestrator] retargeted ComfyUI ${prev} → ${comfyuiUrl} (${isLoopbackUrl(next) ? "local" : "remote"} mode) from panel hello`);
|
|
1957
|
+
// #296: the synchronous `localPathForTarget(next)` above can't see the NEWLY
|
|
1958
|
+
// targeted panel's base_path, so a retarget to a loopback ComfyUI with no
|
|
1959
|
+
// env/auto-detected path lands path-less even though that panel serves it. Kick
|
|
1960
|
+
// off a best-effort recovery from the NEW target's status route (fire-and-forget
|
|
1961
|
+
// so the retarget stays synchronous). On success — and only if a newer retarget
|
|
1962
|
+
// hasn't superseded us mid-fetch — adopt the path and rebuild the comfyui MCP
|
|
1963
|
+
// spawn env so each tab's child respawns in LOCAL mode. Skipped entirely when a
|
|
1964
|
+
// local path is already set (the common case does no I/O).
|
|
1965
|
+
if (!comfyuiPath) {
|
|
1966
|
+
void (async () => {
|
|
1967
|
+
const recovered = await resolveComfyuiPathForTarget({
|
|
1968
|
+
target: next,
|
|
1969
|
+
localPath: localComfyuiPath,
|
|
1970
|
+
forceRemote: isForceRemoteFlagSet(),
|
|
1971
|
+
isLoopback: isLoopbackUrl(next),
|
|
1972
|
+
});
|
|
1973
|
+
if (!recovered)
|
|
1974
|
+
return;
|
|
1975
|
+
// Superseded by a newer retarget while we fetched — do nothing.
|
|
1976
|
+
if (canonTargetUrl(comfyuiUrl) !== canonTargetUrl(next))
|
|
1977
|
+
return;
|
|
1978
|
+
if (comfyuiPath === recovered)
|
|
1979
|
+
return;
|
|
1980
|
+
comfyuiPath = recovered;
|
|
1981
|
+
logger.info(`[panel-orchestrator] recovered ComfyUI base_path from panel status route ` +
|
|
1982
|
+
`after retarget: ${recovered} (#296).`);
|
|
1983
|
+
// Rebuild the comfyui MCP spawn env (now carrying COMFYUI_PATH) and respawn
|
|
1984
|
+
// each tab's agent at its next idle so the LIVE child runs in LOCAL mode.
|
|
1985
|
+
manager.setMcpServers(buildMcpServers());
|
|
1986
|
+
manager.restartAllForMcpEnv();
|
|
1987
|
+
})();
|
|
1988
|
+
}
|
|
1859
1989
|
// Advertising itself happens unconditionally on every hello (see below) —
|
|
1860
1990
|
// a retarget doesn't need its own advertise call here.
|
|
1861
1991
|
return true;
|
|
@@ -1868,10 +1998,23 @@ export async function runPanelOrchestrator() {
|
|
|
1868
1998
|
// idle so the LIVE comfyui MCP subprocess is recreated WITH the new env, and
|
|
1869
1999
|
// nudge it to retry the action the secret unblocked. No process restart, no
|
|
1870
2000
|
// reload fight. The value is never logged — only the env-var KEYS.
|
|
1871
|
-
const
|
|
2001
|
+
const SECRET_RETRY_NUDGE = "🔑 The API token you just provided is now active for the comfyui tools — retry the action that needed it (e.g. the download that returned 401).";
|
|
2002
|
+
const unsubscribeSecrets = onComfyuiSecretsChanged((change) => {
|
|
1872
2003
|
manager.setMcpServers(buildMcpServers());
|
|
1873
|
-
|
|
1874
|
-
|
|
2004
|
+
// EVERY tab's comfyui child needs the new/removed env, so respawn each at its
|
|
2005
|
+
// next idle — but SILENTLY (no nudge). The env rebuild is shared; the "retry
|
|
2006
|
+
// the action" nudge is not, so it must never broadcast to unrelated tabs.
|
|
2007
|
+
// restartAllForMcpEnv() is nudge-preserving, so this can't erase a per-request
|
|
2008
|
+
// nudge already queued on another tab (#164).
|
|
2009
|
+
manager.restartAllForMcpEnv();
|
|
2010
|
+
// NUDGE only the tab whose panel_request_secret this change answers — a
|
|
2011
|
+
// Settings slot save, a background token (re)load, or a revoke leaves
|
|
2012
|
+
// `requested` false and nudges nothing. The per-tab pending-restart map
|
|
2013
|
+
// coalesces, so a repeat event for the same tab can't double-inject (#164).
|
|
2014
|
+
const nudgedTab = change.requested && change.tabId ? change.tabId : null;
|
|
2015
|
+
if (nudgedTab)
|
|
2016
|
+
manager.restartForMcpEnv(agentKeyFor(nudgedTab), SECRET_RETRY_NUDGE);
|
|
2017
|
+
logger.info(`[panel-orchestrator] tool secret ${change.requested ? "saved (requested)" : "changed"} → comfyui MCP env updated + agents respawn on idle${nudgedTab ? ` + retry nudge → tab ${nudgedTab.slice(0, 8)}` : ""} (keys: ${comfyuiSecretKeys().join(", ") || "none"})`);
|
|
1875
2018
|
});
|
|
1876
2019
|
// An agent-provider secret changed (e.g. the OpenRouter API key set from the
|
|
1877
2020
|
// panel). Hydrate it into env, drop the cached openrouter probe/model list so
|
|
@@ -2206,6 +2349,12 @@ export async function runPanelOrchestrator() {
|
|
|
2206
2349
|
tabBackends.delete(migratedFrom);
|
|
2207
2350
|
headlessTabs.delete(migratedFrom);
|
|
2208
2351
|
workflowTargets.clear(migratedFrom);
|
|
2352
|
+
// Invalidate any in-flight set_workflow_target(pinned) resolution captured under
|
|
2353
|
+
// the retired id: dropping its sequence makes isCurrent() false, so a late async
|
|
2354
|
+
// pin can no longer write a stale target / emit a late ack that the bridge's
|
|
2355
|
+
// migration map would deliver to the new tab (codex race, tab-id migration edge).
|
|
2356
|
+
workflowTargetSeq.delete(migratedFrom);
|
|
2357
|
+
tabStableKey.delete(migratedFrom); // recomputed for the new id below (#570)
|
|
2209
2358
|
}
|
|
2210
2359
|
// Blind content mode rides the hello (issue #90) so the FIRST agent spawn
|
|
2211
2360
|
// already carries the right tool-server env. A CHANGE against a live
|
|
@@ -2251,6 +2400,53 @@ export async function runPanelOrchestrator() {
|
|
|
2251
2400
|
const resume = typeof event.resume === "string" ? event.resume : undefined;
|
|
2252
2401
|
if (resume && (!prev || prev === backend))
|
|
2253
2402
|
manager.setResume(key, resume);
|
|
2403
|
+
// #570: an UNSAVED workflow's tab id is an ephemeral tmp:<uuid>, regenerated
|
|
2404
|
+
// on every panel reload — so on an orchestrator restart that also reloads the
|
|
2405
|
+
// panel, the tab returns under a never-stored id and forgets everything
|
|
2406
|
+
// (neither our tab-keyed store nor the panel's tab-keyed hello.resume can hit).
|
|
2407
|
+
// Anchor a STABLE resume key on what DOES survive that tab's reload — the
|
|
2408
|
+
// ComfyUI origin + workflow title + backend — and seed it as a fallback.
|
|
2409
|
+
// Saved workflows (wf:<path>) already key stably, so this is tmp:-only.
|
|
2410
|
+
if (panelTab.startsWith("tmp:")) {
|
|
2411
|
+
const origin = (typeof helloUrl === "string" && helloUrl.trim() ? helloUrl.trim() : undefined) ??
|
|
2412
|
+
bridge.tabOrigin(panelTab) ??
|
|
2413
|
+
"";
|
|
2414
|
+
const title = typeof event.title === "string" ? event.title : "";
|
|
2415
|
+
const skey = `tmp::${origin}::${title}::${backend}`;
|
|
2416
|
+
tabStableKey.set(panelTab, skey);
|
|
2417
|
+
// Seed the resume fallback ONLY when the panel supplied none and no live
|
|
2418
|
+
// agent owns the key. spawn() prefers the exact-tab store hit over this
|
|
2419
|
+
// pendingResume, so the precise path is never overridden — this only
|
|
2420
|
+
// rescues the churned tmp: id. setResume() no-ops if a live agent exists.
|
|
2421
|
+
//
|
|
2422
|
+
// COLLISION GUARD: origin+title+backend is NOT unique — two unsaved tabs
|
|
2423
|
+
// with the same (default) title collide. Seed only when THIS tab is the sole
|
|
2424
|
+
// connected holder of the key, so a fresh sibling tab can never resume
|
|
2425
|
+
// another CONCURRENTLY-live unsaved tab's conversation (the cross-tab hijack
|
|
2426
|
+
// the migration path also refuses; setStable poisons a key the moment two
|
|
2427
|
+
// live tabs write distinct sessions to it). When ambiguous we surface fresh —
|
|
2428
|
+
// a lost resume is a mild miss; resuming the WRONG conversation is not.
|
|
2429
|
+
//
|
|
2430
|
+
// The SEQUENTIAL same-title case (tab A closed, a later tab B opens with the
|
|
2431
|
+
// same origin+title+backend) intentionally resumes A: by the only identity
|
|
2432
|
+
// that survives a reload — the workflow identity the issue itself names as the
|
|
2433
|
+
// key — B *is* the same workspace. This is bounded by the GC TTL (an abandoned
|
|
2434
|
+
// session ages out) and always yields to the exact-tab store and the panel's
|
|
2435
|
+
// own hello.resume. A truly-unique panel per-instance id (referenced in #568/
|
|
2436
|
+
// #570's thread) would let us tighten even this — a clean future upgrade.
|
|
2437
|
+
if (!resume && !manager.hasLiveAgent(key) && sessionStore.get(key) === undefined) {
|
|
2438
|
+
let sameKeyTabs = 0;
|
|
2439
|
+
for (const t of bridge.tabs()) {
|
|
2440
|
+
if (tabStableKey.get(t.tab_id) === skey)
|
|
2441
|
+
sameKeyTabs += 1;
|
|
2442
|
+
}
|
|
2443
|
+
if (sameKeyTabs <= 1) {
|
|
2444
|
+
const stableSid = sessionStore.getStable(skey);
|
|
2445
|
+
if (stableSid)
|
|
2446
|
+
manager.setResume(key, stableSid);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2254
2450
|
// Live model list for the picker; SDK slash commands are Claude-only.
|
|
2255
2451
|
pushModels(panelTab);
|
|
2256
2452
|
// Truthful provider readiness (this machine runs the agents), so the
|
|
@@ -2383,32 +2579,13 @@ export async function runPanelOrchestrator() {
|
|
|
2383
2579
|
}
|
|
2384
2580
|
// Greet only on a FRESH session (a resume/reconnect already has the thread).
|
|
2385
2581
|
if (!resume) {
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
|
|
2394
|
-
: isAg
|
|
2395
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google AI subscription via Antigravity CLI. Note: agy turns show final answers only (no live tool progress). Ask away.`
|
|
2396
|
-
: isGk
|
|
2397
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Grok (xAI) account. Ask away.`
|
|
2398
|
-
: isOl
|
|
2399
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via Ollama (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
|
|
2400
|
-
: isLs
|
|
2401
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via LM Studio (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
|
|
2402
|
-
: isLc
|
|
2403
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} running locally via llama.cpp (no account, no API key). Small local models are slower and simpler than frontier ones — expect fewer frills. Ask away.`
|
|
2404
|
-
: isCu
|
|
2405
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} via your custom endpoint (${customBaseUrl}). Ask away.`
|
|
2406
|
-
: isOr
|
|
2407
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
|
|
2408
|
-
: isCp
|
|
2409
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your GitHub Copilot subscription (⚠️ experimental, ToS risk — you opted in). Ask away.`
|
|
2410
|
-
: `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
|
|
2411
|
-
bridge.push({ type: "say", text: readyText }, panelTab);
|
|
2582
|
+
// Prefer an ALREADY-resolved model if the SDK init raced ahead of this
|
|
2583
|
+
// greeting (#376) — otherwise the pre-init label. Remember what we
|
|
2584
|
+
// advertised so the onSession correction re-sends only on a real
|
|
2585
|
+
// mismatch.
|
|
2586
|
+
const bannerLabel = resolvedModelByTab.get(panelTab) ?? agentLabel;
|
|
2587
|
+
advertisedBannerModel.set(panelTab, bannerLabel);
|
|
2588
|
+
bridge.push({ type: "say", text: readyBannerText(backend, bannerLabel, customBaseUrl) }, panelTab);
|
|
2412
2589
|
}
|
|
2413
2590
|
bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
|
|
2414
2591
|
logger.debug(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) — agent healthy, ready ack`);
|
|
@@ -2506,10 +2683,48 @@ export async function runPanelOrchestrator() {
|
|
|
2506
2683
|
bridge.push({ type: "ack", ok: false, kind: "workflow_target", message: "path required when pinning" }, panelTab);
|
|
2507
2684
|
return;
|
|
2508
2685
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2686
|
+
// Every target event bumps the tab's sequence so a later selection always wins over
|
|
2687
|
+
// an in-flight (async) pin resolution.
|
|
2688
|
+
const seq = (workflowTargetSeq.get(panelTab) ?? 0) + 1;
|
|
2689
|
+
workflowTargetSeq.set(panelTab, seq);
|
|
2690
|
+
const isCurrent = () => workflowTargetSeq.get(panelTab) === seq;
|
|
2691
|
+
const ackTarget = (t) => {
|
|
2692
|
+
bridge.push({ type: "ack", ok: true, kind: "workflow_target", target: t }, panelTab);
|
|
2693
|
+
bridge.push({ type: "workflow_target", target: t }, panelTab);
|
|
2694
|
+
logger.info(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} workflow target → ${t.mode}${t.path ? ` (${t.path})` : ""}`);
|
|
2695
|
+
};
|
|
2696
|
+
// A PINNED target must clear the SAME validation as the MCP tool
|
|
2697
|
+
// (panel_set_workflow_target) — otherwise this panel-driven event path would be a
|
|
2698
|
+
// bypass that re-admits #556/#571 (background pin) and #259 (not-open pin). Resolve
|
|
2699
|
+
// async through the shared helper, failing at pin time before the store is written.
|
|
2700
|
+
if (mode === "pinned") {
|
|
2701
|
+
void (async () => {
|
|
2702
|
+
const ctx = makePanelToolCtx(bridge, panelTab, workflowTargets);
|
|
2703
|
+
const res = await resolvePinTarget(ctx, String(path), filename);
|
|
2704
|
+
// Superseded by a newer target event while we were validating — drop silently
|
|
2705
|
+
// (no write, no late ack) so the newer selection is never clobbered.
|
|
2706
|
+
if (!isCurrent())
|
|
2707
|
+
return;
|
|
2708
|
+
if (!res.ok) {
|
|
2709
|
+
bridge.push({ type: "ack", ok: false, kind: "workflow_target", message: res.error }, panelTab);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
ackTarget(workflowTargets.set(panelTab, { mode: "pinned", path: res.pinPath, filename: res.pinFilename }));
|
|
2713
|
+
})().catch((err) => {
|
|
2714
|
+
if (!isCurrent())
|
|
2715
|
+
return;
|
|
2716
|
+
bridge.push({
|
|
2717
|
+
type: "ack",
|
|
2718
|
+
ok: false,
|
|
2719
|
+
kind: "workflow_target",
|
|
2720
|
+
message: `Could not pin workflow: ${err instanceof Error ? err.message : String(err)}`,
|
|
2721
|
+
}, panelTab);
|
|
2722
|
+
});
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
// mode === "current" — no target to validate; follow the active tab. Writes
|
|
2726
|
+
// synchronously and is the latest sequence, so it wins over any in-flight pin.
|
|
2727
|
+
ackTarget(workflowTargets.set(panelTab, { mode, path, filename }));
|
|
2513
2728
|
return;
|
|
2514
2729
|
}
|
|
2515
2730
|
// Live panel config: render-stall threshold, plus the user's agent-model
|
|
@@ -2966,6 +3181,12 @@ export async function runPanelOrchestrator() {
|
|
|
2966
3181
|
// reset() is synchronous (map cleared now), so no concurrent send() can
|
|
2967
3182
|
// spawn an agent before we report the cleared session.
|
|
2968
3183
|
manager.reset(agentKeyFor(tabId));
|
|
3184
|
+
// reset() clears the exact-tab store; the stable resume index (#570) is the
|
|
3185
|
+
// manager's blind spot, so drop it here too — a deliberate NEW chat must not
|
|
3186
|
+
// be resurrected by the unsaved-workflow fallback on the next reload.
|
|
3187
|
+
const sk = tabStableKey.get(tabId);
|
|
3188
|
+
if (sk)
|
|
3189
|
+
sessionStore.clearStable(sk);
|
|
2969
3190
|
bridge.push({ type: "session", session_id: null }, tabId);
|
|
2970
3191
|
bridge.push({ type: "ack", ok: true, kind: "new_session" }, tabId);
|
|
2971
3192
|
bridge.broadcastTabList(); // session cleared → mirror pickers' green dot off
|
|
@@ -3208,6 +3429,37 @@ export async function runPanelOrchestrator() {
|
|
|
3208
3429
|
// a downloading row that stops updating for 60s is treated as a dead writer.
|
|
3209
3430
|
const DOWNLOAD_LINGER_MS = 8000;
|
|
3210
3431
|
const downloadRemoveAt = new Map();
|
|
3432
|
+
// Download-completion agent events (#547). A finished render already wakes the
|
|
3433
|
+
// agent (manager.injectEvent kind:"executed"); a finished DOWNLOAD had no
|
|
3434
|
+
// equivalent, so a "download then use the model" task stalled until the user
|
|
3435
|
+
// poked it. We observe the SAME first-terminal transition the tray-prune timer
|
|
3436
|
+
// uses and inject a completion event to the tab's agent — but COALESCED: an
|
|
3437
|
+
// apply_manifest that pulls many files would otherwise fire one turn per file,
|
|
3438
|
+
// so completions accumulate per agent for a short window and flush as ONE event.
|
|
3439
|
+
const DOWNLOAD_DONE_DEBOUNCE_MS = 1500;
|
|
3440
|
+
// agentKey → { download identity → {name, terminal status} } accumulated since
|
|
3441
|
+
// the last flush, plus the epoch-ms deadline (extended by each new completion)
|
|
3442
|
+
// at which we emit. Keyed by download IDENTITY (row.id), not display name, so
|
|
3443
|
+
// two distinct downloads sharing a filename in one batch don't overwrite each
|
|
3444
|
+
// other and hide a failure (codex).
|
|
3445
|
+
const downloadDonePending = new Map();
|
|
3446
|
+
// Resolve which agent to wake for a settled download row: the stamped tab's
|
|
3447
|
+
// agent when it's still live; else the SINGLE live agent (pre-fix/in-process
|
|
3448
|
+
// rows carry no tab, AND a tab-id migration/backend change can leave the
|
|
3449
|
+
// stamped tab's key no longer resolving to the live agent — codex); else none —
|
|
3450
|
+
// never fan out to unrelated tabs (#547).
|
|
3451
|
+
const resolveDownloadAgentKey = (row) => {
|
|
3452
|
+
const tab = typeof row.tab === "string" ? row.tab.trim() : "";
|
|
3453
|
+
if (tab) {
|
|
3454
|
+
const key = agentKeyFor(tab);
|
|
3455
|
+
if (manager.hasLiveAgent(key))
|
|
3456
|
+
return key;
|
|
3457
|
+
// Stamped tab's agent is gone (migration/backend switch) — fall through to
|
|
3458
|
+
// the single-live-agent fallback rather than silently dropping the event.
|
|
3459
|
+
}
|
|
3460
|
+
const live = manager.liveKeys();
|
|
3461
|
+
return live.length === 1 ? live[0] : null;
|
|
3462
|
+
};
|
|
3211
3463
|
/** Boolean URL probe with a timeout — readiness checks for pending pod connects. */
|
|
3212
3464
|
const probeOk = async (url, timeoutMs = 8_000) => {
|
|
3213
3465
|
const ctl = new AbortController();
|
|
@@ -3316,6 +3568,19 @@ export async function runPanelOrchestrator() {
|
|
|
3316
3568
|
const due = downloadRemoveAt.get(full);
|
|
3317
3569
|
if (due == null) {
|
|
3318
3570
|
downloadRemoveAt.set(full, now + DOWNLOAD_LINGER_MS); // start the linger
|
|
3571
|
+
// FIRST terminal observation of this download (due was unset) — the
|
|
3572
|
+
// exact once-per-download moment. Wake the tab's agent with the result
|
|
3573
|
+
// (#547), coalesced via downloadDonePending so a many-file manifest is
|
|
3574
|
+
// one turn, not N.
|
|
3575
|
+
const key = resolveDownloadAgentKey(row);
|
|
3576
|
+
if (key && manager.hasLiveAgent(key)) {
|
|
3577
|
+
const bucket = downloadDonePending.get(key) ??
|
|
3578
|
+
{ downloads: new Map(), flushAt: 0 };
|
|
3579
|
+
const idKey = String(row.id ?? full);
|
|
3580
|
+
bucket.downloads.set(idKey, { name: String(row.name ?? row.id ?? "model"), status: String(status) });
|
|
3581
|
+
bucket.flushAt = now + DOWNLOAD_DONE_DEBOUNCE_MS;
|
|
3582
|
+
downloadDonePending.set(key, bucket);
|
|
3583
|
+
}
|
|
3319
3584
|
}
|
|
3320
3585
|
else if (now >= due) {
|
|
3321
3586
|
try {
|
|
@@ -3349,6 +3614,16 @@ export async function runPanelOrchestrator() {
|
|
|
3349
3614
|
lastDownloadSnapshot = snapshot;
|
|
3350
3615
|
bridge.push({ type: "download_progress", downloads }); // broadcast to all tabs
|
|
3351
3616
|
}
|
|
3617
|
+
// Flush any download-completion buckets whose debounce window has elapsed —
|
|
3618
|
+
// ONE agent event per tab per batch of settled downloads (#547). Runs every
|
|
3619
|
+
// tick (700ms) so a flush always fires shortly after the last file settles.
|
|
3620
|
+
for (const [key, bucket] of [...downloadDonePending]) {
|
|
3621
|
+
if (now < bucket.flushAt)
|
|
3622
|
+
continue;
|
|
3623
|
+
downloadDonePending.delete(key);
|
|
3624
|
+
const settled = [...bucket.downloads.values()];
|
|
3625
|
+
manager.injectEvent(key, { kind: "download_done", downloads: settled });
|
|
3626
|
+
}
|
|
3352
3627
|
// MCP-child control channel (#269): runpod_* tools that ran in spawned
|
|
3353
3628
|
// agent children ask the orchestrator to retarget / watch / unwatch /
|
|
3354
3629
|
// auto-connect here — through the SAME applyComfyuiUrl fan-out as a panel
|