comfyui-mcp 0.14.0 → 0.16.0

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.
Files changed (295) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/PACK-SPLIT-STATUS.md +87 -0
  3. package/README.md +10 -4
  4. package/assets/controlnet_demo.png +0 -0
  5. package/assets/ideogram_demo.png +0 -0
  6. package/assets/ltx_sharp_demo.png +0 -0
  7. package/assets/ltx_t2v_demo.png +0 -0
  8. package/assets/qwen_edit_demo.png +0 -0
  9. package/assets/sample_woman.png +0 -0
  10. package/assets/sample_woman_video.mp4 +0 -0
  11. package/assets/wan_demo.png +0 -0
  12. package/assets/wan_sharp_demo.png +0 -0
  13. package/assets/wan_transparent_demo.png +0 -0
  14. package/assets/wan_v2v_demo.png +0 -0
  15. package/dist/comfyui/client.js +39 -4
  16. package/dist/comfyui/client.js.map +1 -1
  17. package/dist/index.js +3 -65
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestrator/index.js +547 -27
  20. package/dist/orchestrator/index.js.map +1 -1
  21. package/dist/orchestrator/panel-agent.js +814 -59
  22. package/dist/orchestrator/panel-agent.js.map +1 -1
  23. package/dist/orchestrator/panel-tools.js +437 -0
  24. package/dist/orchestrator/panel-tools.js.map +1 -0
  25. package/dist/services/download-cache.js +48 -9
  26. package/dist/services/download-cache.js.map +1 -1
  27. package/dist/services/download-progress.js +57 -0
  28. package/dist/services/download-progress.js.map +1 -0
  29. package/dist/services/extra-paths.js +208 -0
  30. package/dist/services/extra-paths.js.map +1 -0
  31. package/dist/services/job-watcher.js +28 -1
  32. package/dist/services/job-watcher.js.map +1 -1
  33. package/dist/services/model-resolver.js +23 -7
  34. package/dist/services/model-resolver.js.map +1 -1
  35. package/dist/services/node-management.js +99 -49
  36. package/dist/services/node-management.js.map +1 -1
  37. package/dist/services/panel-settings.js +53 -0
  38. package/dist/services/panel-settings.js.map +1 -0
  39. package/dist/services/queue-manager.js +98 -9
  40. package/dist/services/queue-manager.js.map +1 -1
  41. package/dist/services/ui-bridge.js +40 -9
  42. package/dist/services/ui-bridge.js.map +1 -1
  43. package/dist/services/user-mcp-config.js +111 -0
  44. package/dist/services/user-mcp-config.js.map +1 -0
  45. package/dist/services/workflow-converter.js +420 -62
  46. package/dist/services/workflow-converter.js.map +1 -1
  47. package/dist/tools/diagnostics.js +22 -9
  48. package/dist/tools/diagnostics.js.map +1 -1
  49. package/dist/tools/extra-paths.js +87 -0
  50. package/dist/tools/extra-paths.js.map +1 -0
  51. package/dist/tools/index.js +4 -0
  52. package/dist/tools/index.js.map +1 -1
  53. package/dist/tools/queue-management.js +65 -3
  54. package/dist/tools/queue-management.js.map +1 -1
  55. package/dist/tools/report-issue.js +56 -0
  56. package/dist/tools/report-issue.js.map +1 -0
  57. package/dist/tools/workflow-library.js +6 -3
  58. package/dist/tools/workflow-library.js.map +1 -1
  59. package/dist/transport/cli.js +1 -8
  60. package/dist/transport/cli.js.map +1 -1
  61. package/model-settings.json +1 -1
  62. package/package.json +5 -2
  63. package/packs/anima/install-runpod.sh +12 -1
  64. package/packs/anima/install-windows.bat +11 -1
  65. package/packs/anima/workflow.json +87 -87
  66. package/packs/anima-img2img/install-runpod.sh +56 -0
  67. package/packs/anima-img2img/install-windows.bat +61 -0
  68. package/packs/anima-img2img/manifest.yaml +69 -0
  69. package/packs/anima-img2img/pack.yaml +30 -0
  70. package/packs/anima-img2img/workflow.json +7076 -0
  71. package/packs/anima-inpaint/install-runpod.sh +49 -0
  72. package/packs/anima-inpaint/install-windows.bat +54 -0
  73. package/packs/anima-inpaint/manifest.yaml +50 -0
  74. package/packs/anima-inpaint/pack.yaml +29 -0
  75. package/packs/anima-inpaint/workflow.json +2009 -0
  76. package/packs/anima-txt2img/install-runpod.sh +55 -0
  77. package/packs/anima-txt2img/install-windows.bat +60 -0
  78. package/packs/anima-txt2img/manifest.yaml +62 -0
  79. package/packs/anima-txt2img/pack.yaml +28 -0
  80. package/packs/anima-txt2img/workflow.json +6442 -0
  81. package/packs/cozy-flow/install-runpod.sh +12 -1
  82. package/packs/cozy-flow/install-windows.bat +11 -1
  83. package/packs/ernie/_slices/combo.json +9661 -0
  84. package/packs/ernie/_slices/img2img.json +2568 -0
  85. package/packs/ernie/_slices/txt2img.json +2379 -0
  86. package/packs/ernie/install-runpod.sh +18 -1
  87. package/packs/ernie/install-windows.bat +17 -1
  88. package/packs/ernie/manifest.yaml +22 -4
  89. package/packs/ernie/pack.yaml +12 -5
  90. package/packs/ernie/workflow.json +1 -1
  91. package/packs/ernie-combo/install-runpod.sh +50 -0
  92. package/packs/ernie-combo/install-windows.bat +55 -0
  93. package/packs/ernie-combo/manifest.yaml +40 -0
  94. package/packs/ernie-combo/pack.yaml +37 -0
  95. package/packs/ernie-combo/workflow.json +9661 -0
  96. package/packs/ernie-img2img/install-runpod.sh +47 -0
  97. package/packs/ernie-img2img/install-windows.bat +52 -0
  98. package/packs/ernie-img2img/manifest.yaml +35 -0
  99. package/packs/ernie-img2img/pack.yaml +35 -0
  100. package/packs/ernie-img2img/workflow.json +2568 -0
  101. package/packs/ernie-txt2img/install-runpod.sh +47 -0
  102. package/packs/ernie-txt2img/install-windows.bat +52 -0
  103. package/packs/ernie-txt2img/manifest.yaml +35 -0
  104. package/packs/ernie-txt2img/pack.yaml +38 -0
  105. package/packs/ernie-txt2img/workflow.json +2379 -0
  106. package/packs/ideogram/install-runpod.sh +12 -1
  107. package/packs/ideogram/install-windows.bat +11 -1
  108. package/packs/ideogram-img2img/install-runpod.sh +40 -0
  109. package/packs/ideogram-img2img/install-windows.bat +45 -0
  110. package/packs/ideogram-img2img/manifest.yaml +34 -0
  111. package/packs/ideogram-img2img/pack.yaml +32 -0
  112. package/packs/ideogram-img2img/workflow.json +3124 -0
  113. package/packs/ideogram-txt2img/install-runpod.sh +40 -0
  114. package/packs/ideogram-txt2img/install-windows.bat +45 -0
  115. package/packs/ideogram-txt2img/manifest.yaml +33 -0
  116. package/packs/ideogram-txt2img/pack.yaml +30 -0
  117. package/packs/ideogram-txt2img/workflow.json +3041 -0
  118. package/packs/ltx-2.3/install-runpod.sh +12 -1
  119. package/packs/ltx-2.3/install-windows.bat +11 -1
  120. package/packs/ltx-2.3-extender/install-runpod.sh +55 -0
  121. package/packs/ltx-2.3-extender/install-windows.bat +60 -0
  122. package/packs/ltx-2.3-extender/manifest.yaml +58 -0
  123. package/packs/ltx-2.3-extender/pack.yaml +20 -0
  124. package/packs/ltx-2.3-extender/workflow.json +5772 -0
  125. package/packs/ltx-2.3-extender-no-audio/install-runpod.sh +55 -0
  126. package/packs/ltx-2.3-extender-no-audio/install-windows.bat +60 -0
  127. package/packs/ltx-2.3-extender-no-audio/manifest.yaml +58 -0
  128. package/packs/ltx-2.3-extender-no-audio/pack.yaml +20 -0
  129. package/packs/ltx-2.3-extender-no-audio/workflow.json +5968 -0
  130. package/packs/ltx-2.3-flf/install-runpod.sh +55 -0
  131. package/packs/ltx-2.3-flf/install-windows.bat +60 -0
  132. package/packs/ltx-2.3-flf/manifest.yaml +58 -0
  133. package/packs/ltx-2.3-flf/pack.yaml +20 -0
  134. package/packs/ltx-2.3-flf/workflow.json +6173 -0
  135. package/packs/ltx-2.3-img2vid/install-runpod.sh +40 -0
  136. package/packs/ltx-2.3-img2vid/install-windows.bat +45 -0
  137. package/packs/ltx-2.3-img2vid/manifest.yaml +41 -0
  138. package/packs/ltx-2.3-img2vid/pack.yaml +20 -0
  139. package/packs/ltx-2.3-img2vid/workflow.json +5062 -0
  140. package/packs/ltx-2.3-txt2vid/install-runpod.sh +40 -0
  141. package/packs/ltx-2.3-txt2vid/install-windows.bat +45 -0
  142. package/packs/ltx-2.3-txt2vid/manifest.yaml +40 -0
  143. package/packs/ltx-2.3-txt2vid/pack.yaml +20 -0
  144. package/packs/ltx-2.3-txt2vid/workflow.json +1 -0
  145. package/packs/ltx-2.3-xy-plot/install-runpod.sh +55 -0
  146. package/packs/ltx-2.3-xy-plot/install-windows.bat +60 -0
  147. package/packs/ltx-2.3-xy-plot/manifest.yaml +58 -0
  148. package/packs/ltx-2.3-xy-plot/pack.yaml +24 -0
  149. package/packs/ltx-2.3-xy-plot/workflow.json +8103 -0
  150. package/packs/qwen-image/install-runpod.sh +12 -1
  151. package/packs/qwen-image/install-windows.bat +11 -1
  152. package/packs/qwen-image-edit/install-runpod.sh +12 -1
  153. package/packs/qwen-image-edit/install-windows.bat +11 -1
  154. package/packs/qwen-image-edit-edit/install-runpod.sh +47 -0
  155. package/packs/qwen-image-edit-edit/install-windows.bat +52 -0
  156. package/packs/qwen-image-edit-edit/manifest.yaml +68 -0
  157. package/packs/qwen-image-edit-edit/pack.yaml +34 -0
  158. package/packs/qwen-image-edit-edit/workflow.json +980 -0
  159. package/packs/wan-animate/install-runpod.sh +12 -1
  160. package/packs/wan-animate/install-windows.bat +11 -1
  161. package/packs/wan-animate-character/install-runpod.sh +48 -0
  162. package/packs/wan-animate-character/install-windows.bat +53 -0
  163. package/packs/wan-animate-character/manifest.yaml +53 -0
  164. package/packs/wan-animate-character/pack.yaml +44 -0
  165. package/packs/wan-animate-character/workflow.json +1 -0
  166. package/packs/wan-longer-videos/install-runpod.sh +12 -1
  167. package/packs/wan-longer-videos/install-windows.bat +11 -1
  168. package/packs/wan-longer-videos-i2v/install-runpod.sh +44 -0
  169. package/packs/wan-longer-videos-i2v/install-windows.bat +49 -0
  170. package/packs/wan-longer-videos-i2v/manifest.yaml +39 -0
  171. package/packs/wan-longer-videos-i2v/pack.yaml +40 -0
  172. package/packs/wan-longer-videos-i2v/workflow.json +1 -0
  173. package/packs/wan-longer-videos-i2v-96gb/install-runpod.sh +44 -0
  174. package/packs/wan-longer-videos-i2v-96gb/install-windows.bat +49 -0
  175. package/packs/wan-longer-videos-i2v-96gb/manifest.yaml +40 -0
  176. package/packs/wan-longer-videos-i2v-96gb/pack.yaml +40 -0
  177. package/packs/wan-longer-videos-i2v-96gb/workflow.json +1 -0
  178. package/packs/wan-longer-videos-t2v/install-runpod.sh +49 -0
  179. package/packs/wan-longer-videos-t2v/install-windows.bat +54 -0
  180. package/packs/wan-longer-videos-t2v/manifest.yaml +50 -0
  181. package/packs/wan-longer-videos-t2v/pack.yaml +40 -0
  182. package/packs/wan-longer-videos-t2v/workflow.json +1 -0
  183. package/packs/wan-longer-videos-t2v-96gb/install-runpod.sh +49 -0
  184. package/packs/wan-longer-videos-t2v-96gb/install-windows.bat +54 -0
  185. package/packs/wan-longer-videos-t2v-96gb/manifest.yaml +50 -0
  186. package/packs/wan-longer-videos-t2v-96gb/pack.yaml +40 -0
  187. package/packs/wan-longer-videos-t2v-96gb/workflow.json +1 -0
  188. package/packs/wan-longer-videos-v2v/install-runpod.sh +44 -0
  189. package/packs/wan-longer-videos-v2v/install-windows.bat +49 -0
  190. package/packs/wan-longer-videos-v2v/manifest.yaml +39 -0
  191. package/packs/wan-longer-videos-v2v/pack.yaml +40 -0
  192. package/packs/wan-longer-videos-v2v/workflow.json +1 -0
  193. package/packs/wan-longer-videos-v2v-96gb/install-runpod.sh +44 -0
  194. package/packs/wan-longer-videos-v2v-96gb/install-windows.bat +49 -0
  195. package/packs/wan-longer-videos-v2v-96gb/manifest.yaml +40 -0
  196. package/packs/wan-longer-videos-v2v-96gb/pack.yaml +40 -0
  197. package/packs/wan-longer-videos-v2v-96gb/workflow.json +1 -0
  198. package/packs/wan-transparent/install-runpod.sh +12 -1
  199. package/packs/wan-transparent/install-windows.bat +11 -1
  200. package/packs/wan-transparent-img2vid/install-runpod.sh +45 -0
  201. package/packs/wan-transparent-img2vid/install-windows.bat +50 -0
  202. package/packs/wan-transparent-img2vid/manifest.yaml +47 -0
  203. package/packs/wan-transparent-img2vid/pack.yaml +44 -0
  204. package/packs/wan-transparent-img2vid/workflow.json +1 -0
  205. package/packs/wan-transparent-img2vid-96gb/install-runpod.sh +45 -0
  206. package/packs/wan-transparent-img2vid-96gb/install-windows.bat +50 -0
  207. package/packs/wan-transparent-img2vid-96gb/manifest.yaml +48 -0
  208. package/packs/wan-transparent-img2vid-96gb/pack.yaml +44 -0
  209. package/packs/wan-transparent-img2vid-96gb/workflow.json +1 -0
  210. package/packs/z-image-base/install-runpod.sh +13 -2
  211. package/packs/z-image-base/install-windows.bat +12 -2
  212. package/packs/z-image-base/manifest.yaml +2 -2
  213. package/packs/z-image-base/workflow.json +1 -1
  214. package/packs/z-image-base-combo/install-runpod.sh +51 -0
  215. package/packs/z-image-base-combo/install-windows.bat +56 -0
  216. package/packs/z-image-base-combo/manifest.yaml +42 -0
  217. package/packs/z-image-base-combo/pack.yaml +31 -0
  218. package/packs/z-image-base-combo/workflow.json +4918 -0
  219. package/packs/z-image-base-controlnet/install-runpod.sh +53 -0
  220. package/packs/z-image-base-controlnet/install-windows.bat +58 -0
  221. package/packs/z-image-base-controlnet/manifest.yaml +51 -0
  222. package/packs/z-image-base-controlnet/pack.yaml +33 -0
  223. package/packs/z-image-base-controlnet/workflow.json +3992 -0
  224. package/packs/z-image-base-img2img/install-runpod.sh +50 -0
  225. package/packs/z-image-base-img2img/install-windows.bat +55 -0
  226. package/packs/z-image-base-img2img/manifest.yaml +37 -0
  227. package/packs/z-image-base-img2img/pack.yaml +29 -0
  228. package/packs/z-image-base-img2img/workflow.json +1621 -0
  229. package/packs/z-image-base-inpaint/install-runpod.sh +47 -0
  230. package/packs/z-image-base-inpaint/install-windows.bat +52 -0
  231. package/packs/z-image-base-inpaint/manifest.yaml +34 -0
  232. package/packs/z-image-base-inpaint/pack.yaml +29 -0
  233. package/packs/z-image-base-inpaint/workflow.json +1780 -0
  234. package/packs/z-image-base-txt2img/install-runpod.sh +50 -0
  235. package/packs/z-image-base-txt2img/install-windows.bat +55 -0
  236. package/packs/z-image-base-txt2img/manifest.yaml +37 -0
  237. package/packs/z-image-base-txt2img/pack.yaml +29 -0
  238. package/packs/z-image-base-txt2img/workflow.json +1416 -0
  239. package/packs/z-image-turbo/install-runpod.sh +13 -2
  240. package/packs/z-image-turbo/install-windows.bat +12 -2
  241. package/packs/z-image-turbo/manifest.yaml +1 -1
  242. package/packs/z-image-turbo/workflow.json +7 -7
  243. package/packs/z-image-turbo-combo/install-runpod.sh +50 -0
  244. package/packs/z-image-turbo-combo/install-windows.bat +55 -0
  245. package/packs/z-image-turbo-combo/manifest.yaml +35 -0
  246. package/packs/z-image-turbo-combo/pack.yaml +31 -0
  247. package/packs/z-image-turbo-combo/workflow.json +2038 -0
  248. package/packs/z-image-turbo-controlnet/install-runpod.sh +52 -0
  249. package/packs/z-image-turbo-controlnet/install-windows.bat +57 -0
  250. package/packs/z-image-turbo-controlnet/manifest.yaml +45 -0
  251. package/packs/z-image-turbo-controlnet/pack.yaml +33 -0
  252. package/packs/z-image-turbo-controlnet/workflow.json +2607 -0
  253. package/packs/z-image-turbo-detail-daemon/install-runpod.sh +50 -0
  254. package/packs/z-image-turbo-detail-daemon/install-windows.bat +55 -0
  255. package/packs/z-image-turbo-detail-daemon/manifest.yaml +35 -0
  256. package/packs/z-image-turbo-detail-daemon/pack.yaml +31 -0
  257. package/packs/z-image-turbo-detail-daemon/workflow.json +1963 -0
  258. package/packs/z-image-turbo-img2img/install-runpod.sh +50 -0
  259. package/packs/z-image-turbo-img2img/install-windows.bat +55 -0
  260. package/packs/z-image-turbo-img2img/manifest.yaml +35 -0
  261. package/packs/z-image-turbo-img2img/pack.yaml +28 -0
  262. package/packs/z-image-turbo-img2img/workflow.json +1285 -0
  263. package/packs/z-image-turbo-inpainting/install-runpod.sh +47 -0
  264. package/packs/z-image-turbo-inpainting/install-windows.bat +52 -0
  265. package/packs/z-image-turbo-inpainting/manifest.yaml +32 -0
  266. package/packs/z-image-turbo-inpainting/pack.yaml +28 -0
  267. package/packs/z-image-turbo-inpainting/workflow.json +1131 -0
  268. package/packs/z-image-turbo-txt2img/install-runpod.sh +44 -0
  269. package/packs/z-image-turbo-txt2img/install-windows.bat +49 -0
  270. package/packs/z-image-turbo-txt2img/manifest.yaml +29 -0
  271. package/packs/z-image-turbo-txt2img/pack.yaml +27 -0
  272. package/packs/z-image-turbo-txt2img/workflow.json +1137 -0
  273. package/packs/z-image-xy-plot/install-runpod.sh +13 -2
  274. package/packs/z-image-xy-plot/install-windows.bat +12 -2
  275. package/packs/z-image-xy-plot/manifest.yaml +1 -1
  276. package/packs/z-image-xy-plot/pack.yaml +1 -1
  277. package/packs/z-image-xy-plot/workflow.json +1 -1
  278. package/plugin/.mcp.json +1 -1
  279. package/plugin/skills/ernie-image/SKILL.md +12 -0
  280. package/plugin/skills/flux-txt2img/SKILL.md +1 -1
  281. package/plugin/skills/ltxv2-video/SKILL.md +51 -0
  282. package/plugin/skills/report-bug/SKILL.md +135 -0
  283. package/plugin/skills/workflow-layout/SKILL.md +114 -0
  284. package/plugin/skills/z-image-txt2img/SKILL.md +3 -3
  285. package/scripts/check-pack-models.mjs +155 -0
  286. package/scripts/gen-pack-installers.mjs +25 -7
  287. package/scripts/gen-tool-docs.ts +3 -1
  288. package/scripts/mock-panel.mjs +156 -0
  289. package/scripts/probe-bridge.mjs +39 -0
  290. package/scripts/probe-models.mjs +18 -0
  291. package/scripts/slice-pipeline.mjs +79 -0
  292. package/scripts/test-agent.mjs +313 -0
  293. package/scripts/verify-render.mjs +103 -0
  294. package/dist/tools/panel.js +0 -249
  295. package/dist/tools/panel.js.map +0 -1
@@ -3,21 +3,100 @@
3
3
  // Claude session stays free. Launch with `comfyui-mcp --panel-orchestrator`
4
4
  // (or COMFYUI_MCP_PANEL_ORCHESTRATOR=1).
5
5
  //
6
- // It owns the UI bridge (port 9101) directly — so it SEES panel messages instead
6
+ // It owns the UI bridge (port 9180) directly — so it SEES panel messages instead
7
7
  // of relying on an idle interactive session to notice a channel push — and spawns
8
8
  // one Claude Agent SDK streaming session per panel tab (src/orchestrator/
9
9
  // panel-agent.ts). Each agent runs on the user's Claude SUBSCRIPTION with no API
10
10
  // key. See docs/design/panel-orchestrator.md.
11
- import { existsSync } from "node:fs";
11
+ import { existsSync, writeFileSync, unlinkSync, readFileSync, readdirSync } from "node:fs";
12
+ import { execFileSync } from "node:child_process";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
12
15
  import { fileURLToPath } from "node:url";
13
16
  import { startUiBridge } from "../services/ui-bridge.js";
14
17
  import { logger } from "../utils/logger.js";
15
- import { PanelAgentManager } from "./panel-agent.js";
18
+ import { PanelAgentManager, fetchSupportedModels, fetchSupportedCommands, isEffort, } from "./panel-agent.js";
19
+ import { createPanelMcpServer } from "./panel-tools.js";
20
+ import { readUserMcpServers } from "../services/user-mcp-config.js";
16
21
  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.
17
22
 
18
- You have the comfyui MCP tools to generate images, video, and audio and to inspect and manage their ComfyUI instance. Use them to actually do what's asked, then tell them what you did and name or link any output. If a request is ambiguous, make a sensible choice and say what you chose rather than stalling.
23
+ You can SEE and EDIT the workflow the user currently has open, via the panel_* tools (panel_get_graph, panel_add_node, panel_connect, panel_set_widget, panel_run, panel_get_errors, panel_save_workflow, …). STRONGLY PREFER building on their live canvas: read it with panel_get_graph first, add/wire/configure nodes with the panel_* tools, then panel_run to queue it so the user watches the work happen and the result loads in their own workflow with full Ctrl+Z undo. Only fall back to the headless generate_image/enqueue_workflow tools when the user explicitly wants a one-off they don't need on their canvas, or when no panel tab is connected (a panel_* call will error if so).
19
24
 
20
- You are running in the background on the user's own machine. For routine, reversible actions that follow from the request, act without asking permission.`;
25
+ If a workflow needs a custom node the user doesn't have, don't silently skip it — offer to install it. Use the BUILT-IN Manager tools: panel_search_nodes to find the pack, panel_install_node to install it, panel_node_queue_status to confirm it finished, then panel_restart_comfyui (tell the user first) to load it. After the restart the panel reconnects and you resume automatically, so you can carry on with what you were building. Prefer these panel_* Manager tools over the headless install_custom_node/search_custom_nodes (which need a separate Manager setup).
26
+
27
+ 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_get_graph and the panel_* edit tools operate on the subgraph's inner nodes — and panel_exit_subgraph when you're done.
28
+
29
+ You also have the comfyui MCP tools to generate images, video, and audio and to inspect, download models for, and manage their ComfyUI instance. Use them to actually do what's asked, then tell them what you did and name or link any output. If a request is ambiguous, make a sensible choice and say what you chose rather than stalling.
30
+
31
+ You are running in the background on the user's own machine. For routine, reversible actions that follow from the request, act without asking permission.
32
+
33
+ You can extend your own capabilities by connecting MCP servers: panel_list_mcp shows what's connected, panel_add_mcp writes a new server to the user's Claude config, and panel_remove_mcp removes one — then call panel_reload to load the change into this session (it restarts you and resumes automatically). For example, if a task needs Civitai model search and it isn't connected, offer to add the official CivitAI MCP (transport 'http', url 'https://mcp.civitai.com/mcp'), then reload. ALWAYS ask the user before connecting a remote MCP — it's an external service connection. After editing your own orchestrator/panel code, you can also call panel_reload to pick it up without a ComfyUI restart.
34
+
35
+ When you genuinely need the user to choose between options, use the panel_ask tool — it renders an interactive question card in the panel chat and returns their pick (the card always includes an 'Other…' free-text field, so they can answer freely too). Reserve it for decisions that actually change what you do; for a simple yes/no or quick confirmation a plain-text question in your reply is fine. Do NOT use the built-in AskUserQuestion tool — it does not render in this panel and gets auto-dismissed, which makes you think the user declined.
36
+
37
+ For any genuinely multi-step task, set a plan with panel_set_todo and then WORK THROUGH IT LIKE A LOOP: do each step, mark it done and the next one active as you go, and keep going autonomously all the way to the end. Do NOT stop between steps to ask "should I continue?", to report routine progress, or to seek permission for steps that plainly follow from the plan — the todo list is your commitment to FINISH, not a menu to re-confirm. Clear it (panel_set_todo with an empty array) once every item is done. The ONLY reason to pause the loop is a decision you genuinely cannot make yourself or a true blocker — then use panel_ask to get the answer and immediately resume the loop. When the user hands you a list of things to do, default to executing the WHOLE list before yielding the turn.
38
+
39
+ Adult / NSFW content is gated behind an explicit, persistent consent mode — query it with panel_get_content_mode (it defaults to OFF). ComfyUI has a large community of legitimate adult artists, so adult requests are valid creative work — but only for verified consenting adults. BY DEFAULT (mode OFF) keep everything safe-for-work: don't surface NSFW models, LoRAs, prompts, workflows, or imagery, and prefer SFW results. When a request clearly intends adult/NSFW work and the mode is OFF, call panel_request_adult_consent — it shows a card confirming the user is 18+ and that adult content is legal in their region; only once they affirm does the mode turn on (it persists across reloads, and panel_disable_adult_mode reverts it). When the mode is ON, help with legal adult art for consenting adults and don't over-refuse — stylized/fantasy themes between clearly-adult fictional characters are in scope. ABSOLUTE limits that NO mode, setting, or request ever relaxes: never sexual content involving minors or anyone depicted as underage; never sexual deepfakes of real, identifiable people; never depictions of actual non-consensual sexual acts (rape). If a request crosses these, refuse regardless of the mode.`;
40
+ /**
41
+ * Lockfile path for a given bridge port. The orchestrator self-registers its
42
+ * REAL node pid here (not the npx shim's), plus the ComfyUI pid that launched
43
+ * it, so the panel pack can reliably identify and replace a stale orchestrator
44
+ * left over from a previous ComfyUI session (the "orphan on the port" trap).
45
+ */
46
+ function orchLockPath(port) {
47
+ return join(tmpdir(), `comfyui-mcp-panel-orch-${port}.json`);
48
+ }
49
+ function readWindowsProcessStartedAtMs(pid) {
50
+ const script = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
51
+ `if ($p) { ([Management.ManagementDateTimeConverter]::ToDateTime($p.CreationDate)).ToUniversalTime().ToString("o") }`;
52
+ for (const exe of ["powershell.exe", "powershell"]) {
53
+ try {
54
+ const out = execFileSync(exe, ["-NoProfile", "-NonInteractive", "-Command", script], {
55
+ encoding: "utf8",
56
+ timeout: 2000,
57
+ windowsHide: true,
58
+ }).trim();
59
+ if (!out)
60
+ return null;
61
+ const ms = Date.parse(out);
62
+ return Number.isFinite(ms) ? ms : null;
63
+ }
64
+ catch {
65
+ // Try the next PowerShell executable name.
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ function readProcessStartedAtMs(pid) {
71
+ if (!Number.isInteger(pid) || pid <= 0)
72
+ return null;
73
+ if (process.platform === "win32")
74
+ return readWindowsProcessStartedAtMs(pid);
75
+ return null;
76
+ }
77
+ function pidExists(pid) {
78
+ try {
79
+ process.kill(pid, 0); // signal 0 = existence probe, doesn't actually signal
80
+ return true;
81
+ }
82
+ catch (err) {
83
+ // EPERM = exists but not ours to signal.
84
+ return err.code === "EPERM";
85
+ }
86
+ }
87
+ function parentIdentityMatches(pid, expectedStartedAtMs) {
88
+ if (!pidExists(pid))
89
+ return false;
90
+ if (!expectedStartedAtMs)
91
+ return true; // legacy/manual launch: PID liveness only.
92
+ const actualStartedAtMs = readProcessStartedAtMs(pid);
93
+ // Couldn't read the start time (transient PowerShell failure / no reader): the
94
+ // pid IS alive, so DON'T false-positive "parent gone" and suicide — fall back
95
+ // to liveness. The pack's Connect-time orphan check is the backstop for reuse.
96
+ if (!actualStartedAtMs)
97
+ return true;
98
+ return Math.abs(actualStartedAtMs - expectedStartedAtMs) <= 2000;
99
+ }
21
100
  /**
22
101
  * Tie the orchestrator's lifetime to ComfyUI's. The launcher (the panel pack)
23
102
  * passes its own PID as COMFYUI_MCP_PARENT_PID; we poll whether that process is
@@ -31,16 +110,20 @@ function startParentWatchdog(onParentGone) {
31
110
  const ppid = raw ? Number(raw) : NaN;
32
111
  if (!Number.isInteger(ppid) || ppid <= 0)
33
112
  return;
113
+ const expectedStartedAtMs = Number(process.env.COMFYUI_MCP_PARENT_STARTED_AT_MS) || null;
114
+ // Cheap pid-liveness probe every 5s; the expensive start-time identity check
115
+ // (which shells out to PowerShell on Windows) only every ~30s — enough to
116
+ // catch pid reuse without spawning a process every 5s for the orchestrator's
117
+ // whole life.
118
+ let polls = 0;
34
119
  const timer = setInterval(() => {
35
- let alive = true;
36
- try {
37
- process.kill(ppid, 0); // signal 0 = existence probe, doesn't actually signal
38
- }
39
- catch (err) {
40
- // ESRCH = gone; EPERM = exists but not ours to signal (treat as alive).
41
- alive = err.code === "EPERM";
120
+ polls += 1;
121
+ if (!pidExists(ppid)) {
122
+ clearInterval(timer);
123
+ onParentGone();
124
+ return;
42
125
  }
43
- if (!alive) {
126
+ if (expectedStartedAtMs && polls % 6 === 0 && !parentIdentityMatches(ppid, expectedStartedAtMs)) {
44
127
  clearInterval(timer);
45
128
  onParentGone();
46
129
  }
@@ -54,25 +137,80 @@ function startParentWatchdog(onParentGone) {
54
137
  * process alive until SIGINT/SIGTERM or the parent (ComfyUI) exits.
55
138
  */
56
139
  export async function runPanelOrchestrator() {
140
+ // Crash guard: the orchestrator is a long-lived background process the user
141
+ // can't see. A stray rejection (e.g. a fire-and-forget push to a tab that
142
+ // vanished mid-flight, or an SDK hiccup) must never silently kill it —
143
+ // otherwise the panel goes dead with no explanation. Log and keep running.
144
+ process.on("unhandledRejection", (reason) => {
145
+ // Benign strays are common here (a fire-and-forget push to a tab that vanished
146
+ // mid-flight, an SDK hiccup) and must NOT kill the orchestrator — log + continue.
147
+ logger.error(`[panel-orchestrator] unhandled rejection (ignored): ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
148
+ });
149
+ process.on("uncaughtException", (err) => {
150
+ // A synchronous uncaught throw leaves the process in an UNDEFINED state. The
151
+ // old "log + continue" here was a zombie root cause — the orchestrator stayed
152
+ // alive but broken, so the panel couldn't reconnect and a ComfyUI restart just
153
+ // reattached to it. Exit so the pack respawns a clean orchestrator (Node's own
154
+ // default is to crash on uncaughtException anyway).
155
+ logger.error(`[panel-orchestrator] FATAL uncaught exception — exiting so a fresh orchestrator can take over: ${err.stack ?? err.message}`);
156
+ process.exit(1);
157
+ });
57
158
  // Subscription lane: the background agent must authenticate against the user's
58
159
  // claude.ai login, never an API key. Unset the key for the SDK subprocess.
59
160
  delete process.env.ANTHROPIC_API_KEY;
60
- const bridge = startUiBridge();
161
+ // Dedicated PANEL bridge port (default 9180).
162
+ const bridge = startUiBridge(Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180);
61
163
  // Owning the bridge port is the orchestrator's whole job — if another process
62
- // holds it (e.g. an interactive comfyui-mcp running with --channels), fail
63
- // loudly instead of running uselessly. (This also avoids the case where a
64
- // failed bind leaves the process with no live handles and it exits silently.)
164
+ // holds it, fail loudly instead of running uselessly. (This also avoids the
165
+ // case where a failed bind leaves the process with no live handles and it
166
+ // exits silently.)
65
167
  const bound = await bridge.whenReady();
66
168
  if (!bound) {
67
- logger.error(`[panel-orchestrator] could not bind the panel bridge port — another process owns it (often an interactive comfyui-mcp started with --channels). Free that port (or stop the --channels session) and restart the orchestrator. Override the port with COMFYUI_MCP_BRIDGE_PORT.`);
169
+ logger.error(`[panel-orchestrator] could not bind the panel bridge port — another process owns it. Free that port and restart the orchestrator. Override the port with COMFYUI_MCP_BRIDGE_PORT.`);
68
170
  process.exit(1);
69
171
  }
70
- // The spawned agent runs THIS comfyui-mcp build as its MCP server, in normal
71
- // (non-channels) mode so it generates against the live ComfyUI over
72
- // COMFYUI_URL and never tries to bind the bridge port we own here.
172
+ // We own the port register our REAL pid + the launching ComfyUI pid so the
173
+ // panel pack can detect and replace us if we're ever orphaned across a Comfy
174
+ // restart. Written only after a successful bind (so the file always names the
175
+ // process that actually holds the port).
176
+ const lockPort = Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180;
177
+ const lockPath = orchLockPath(lockPort);
178
+ try {
179
+ writeFileSync(lockPath, JSON.stringify({
180
+ pid: process.pid,
181
+ // Our OWN process creation time, captured now while we KNOW this pid is
182
+ // the real orchestrator. The pack matches a live pid's creation time
183
+ // against this before ever killing it, so a reused pid (a shell, node,
184
+ // anything that inherits our old pid) can't be mistaken for us and
185
+ // terminated — the TOCTOU pid-reuse guard. Null on platforms we can't
186
+ // read it (the pack then falls back to the cmdline identity check).
187
+ pidStartedAt: readProcessStartedAtMs(process.pid),
188
+ parent: Number(process.env.COMFYUI_MCP_PARENT_PID) || null,
189
+ parentStartedAt: Number(process.env.COMFYUI_MCP_PARENT_STARTED_AT_MS) || null,
190
+ port: lockPort,
191
+ startedAt: new Date().toISOString(),
192
+ }));
193
+ }
194
+ catch (err) {
195
+ logger.debug(`[panel-orchestrator] could not write lockfile ${lockPath}: ${err instanceof Error ? err.message : String(err)}`);
196
+ }
197
+ // The spawned agent runs THIS comfyui-mcp build as its MCP server in normal
198
+ // mode — so it generates against the live ComfyUI over COMFYUI_URL and never
199
+ // tries to bind the bridge port we own here.
73
200
  const mcpEntry = fileURLToPath(new URL("../index.js", import.meta.url));
74
201
  const comfyuiUrl = process.env.COMFYUI_URL ?? "http://127.0.0.1:8188";
202
+ // ComfyUI install path — when set, the spawned agent's MCP runs in LOCAL mode,
203
+ // so download_model / apply_manifest / installer-pack / model-scan tools work
204
+ // instead of degrading to remote-only. The panel pack supplies this.
205
+ const comfyuiPath = process.env.COMFYUI_PATH;
75
206
  const model = process.env.COMFYUI_MCP_PANEL_MODEL ?? "claude-opus-4-8";
207
+ const envEffort = process.env.COMFYUI_MCP_PANEL_EFFORT;
208
+ const effort = isEffort(envEffort) ? envEffort : undefined;
209
+ const bridgePort = Number(process.env.COMFYUI_MCP_BRIDGE_PORT) || 9180;
210
+ // Cross-process download-progress channel: each tab's comfyui MCP subprocess
211
+ // writes per-download JSON here; the watcher below broadcasts it to the panel
212
+ // tray. Port-scoped so parallel orchestrators don't cross streams.
213
+ const progressDir = join(tmpdir(), `comfyui-mcp-progress-${bridgePort}`);
76
214
  // The bundled plugin (skills) ships alongside dist/ in the package root. Load
77
215
  // it so the background agents are ComfyUI experts out of the box.
78
216
  const pluginPath = fileURLToPath(new URL("../../plugin", import.meta.url));
@@ -80,23 +218,317 @@ export async function runPanelOrchestrator() {
80
218
  if (!pluginAvailable) {
81
219
  logger.warn(`[panel-orchestrator] bundled plugin not found at ${pluginPath} — agents run without model-expertise skills.`);
82
220
  }
221
+ // Build an agent_status frame from a usage snapshot — used both live (per
222
+ // assistant response) and to re-push the last value when a tab reconnects.
223
+ function pushStatus(tabId, status) {
224
+ bridge.push({
225
+ type: "agent_status",
226
+ ...(typeof status.contextPct === "number" ? { context_pct: status.contextPct } : {}),
227
+ ...(typeof status.used === "number" ? { used: status.used } : {}),
228
+ ...(typeof status.contextWindow === "number" ? { context_window: status.contextWindow } : {}),
229
+ ...(status.model ? { model: status.model } : {}),
230
+ ...(typeof status.costUsd === "number" ? { cost_usd: status.costUsd } : {}),
231
+ }, tabId);
232
+ }
233
+ // Inherit the user's own MCP servers (the same ones their normal `claude`
234
+ // session uses), read from ~/.claude.json. Conflicting comfyui entries are
235
+ // filtered out by the reader so they can't grab our bridge port. This is what
236
+ // makes "add the CivitAI MCP" work: panel_add_mcp writes it here, a reload
237
+ // re-reads it, and the agent gains those tools. Re-read on every (re)start so
238
+ // new servers are picked up on the next soft reload.
239
+ const userMcpServers = readUserMcpServers();
240
+ const userMcpNames = Object.keys(userMcpServers);
241
+ if (userMcpNames.length) {
242
+ logger.info(`[panel-orchestrator] inheriting user MCP servers: ${userMcpNames.join(", ")}`);
243
+ }
83
244
  const manager = new PanelAgentManager({
84
245
  model,
246
+ effort,
247
+ comfyuiUrl, // for fetching image bytes to inline into agent turns
85
248
  systemAppend: PANEL_SYSTEM_APPEND,
86
249
  pluginPath: pluginAvailable ? pluginPath : undefined,
250
+ // Live-graph control of the user's open workflow, per tab (in-process).
251
+ makePanelServer: (tabId) => createPanelMcpServer(bridge, tabId),
87
252
  mcpServers: {
253
+ // The user's inherited servers first…
254
+ ...userMcpServers,
255
+ // …then our own comfyui server LAST, so it always wins over any user
256
+ // entry that slipped through (defensive — the reader already filters them).
88
257
  comfyui: {
89
258
  type: "stdio",
90
259
  command: process.execPath, // node
91
- args: [mcpEntry], // dist/index.js, no --channels
92
- env: { COMFYUI_URL: comfyuiUrl },
260
+ args: [mcpEntry], // dist/index.js
261
+ env: {
262
+ COMFYUI_URL: comfyuiUrl,
263
+ // Where download_model writes live progress for the panel tray.
264
+ COMFYUI_MCP_PROGRESS_DIR: progressDir,
265
+ // Local mode → enables download_model, apply_manifest (installer packs),
266
+ // and model scans so the agent installs the right way instead of curl.
267
+ ...(comfyuiPath ? { COMFYUI_PATH: comfyuiPath } : {}),
268
+ },
93
269
  },
94
270
  },
95
- onSay: (tabId, text) => {
96
- bridge.push({ type: "say", text }, tabId);
271
+ onSay: (tabId, text, meta) => {
272
+ // `id` lets the panel reconcile this committed message with its live
273
+ // streaming preview (same id) instead of rendering a duplicate bubble.
274
+ bridge.push({ type: "say", text, id: meta?.id, streamed: meta?.streamed }, tabId);
275
+ },
276
+ // Live streaming deltas → the panel's think-window + streaming reply bubble.
277
+ onStream: (tabId, ev) => {
278
+ bridge.push({ type: "stream", phase: ev.phase, id: ev.id, delta: ev.delta }, tabId);
279
+ },
280
+ // Per-response usage → the panel's context/usage meter (updates live).
281
+ onStatus: pushStatus,
282
+ // Report the SDK session id so the panel can persist it and resume on reload.
283
+ onSession: (tabId, sessionId) => {
284
+ bridge.push({ type: "session", session_id: sessionId }, tabId);
285
+ },
286
+ // Per-turn rewind anchor (assistant UUID) → the panel stores it so a later
287
+ // "rewind conversation to here" can fork the session at that point.
288
+ onTurnAnchor: (tabId, uuid) => {
289
+ bridge.push({ type: "turn_anchor", uuid }, tabId);
290
+ },
291
+ // Turn lifecycle → the panel's "working" indicator (stays up through silent
292
+ // tool work; clears on done).
293
+ onTurn: (tabId, state) => {
294
+ bridge.push({ type: "turn", state }, tabId);
295
+ },
296
+ // Live extended-thinking token count → "thinking… (N)" indicator.
297
+ onThinking: (tabId, tokens) => {
298
+ bridge.push({ type: "thinking", tokens }, tabId);
299
+ },
300
+ // The agent dequeued a message (the true "read" moment) → flip that bubble
301
+ // from queued/muted to read.
302
+ onSeen: (tabId, mid) => {
303
+ bridge.push({ type: "ack", ok: true, kind: "seen", mid }, tabId);
97
304
  },
98
305
  });
306
+ // Debounce the connect ack: the panel re-sends `hello` on reconnect and on
307
+ // workflow-title changes, which would otherwise stack duplicate greetings.
308
+ const lastAckAt = new Map();
309
+ const ACK_DEBOUNCE_MS = 4000;
310
+ // The account's real model list — probed once from the SDK (the only way that
311
+ // works on the subscription lane) and cached. Pushed to each tab so the
312
+ // panel's model/effort picker reflects what's actually available, with each
313
+ // model's supported effort levels, instead of a hardcoded list.
314
+ let modelsPromise = null;
315
+ function ensureModels() {
316
+ if (!modelsPromise) {
317
+ modelsPromise = fetchSupportedModels(model).then((list) => {
318
+ // Don't cache an empty/failed probe forever — let the next hello retry.
319
+ if (!list.length)
320
+ modelsPromise = null;
321
+ return list;
322
+ });
323
+ }
324
+ return modelsPromise;
325
+ }
326
+ function pushModels(tabId) {
327
+ void ensureModels()
328
+ .then((models) => {
329
+ if (models.length) {
330
+ bridge.push({ type: "models", models, current: model }, tabId);
331
+ }
332
+ })
333
+ .catch(() => {
334
+ /* probe already logged; panel keeps its fallback list */
335
+ });
336
+ }
337
+ // The SDK's slash commands (built-ins like /compact, plus any loaded skills) —
338
+ // probed once and surfaced in the panel composer's completion menu.
339
+ let commandsPromise = null;
340
+ function ensureCommands() {
341
+ if (!commandsPromise) {
342
+ commandsPromise = fetchSupportedCommands(model).then((list) => {
343
+ if (!list.length)
344
+ commandsPromise = null; // let the next hello retry
345
+ return list;
346
+ });
347
+ }
348
+ return commandsPromise;
349
+ }
350
+ // The SDK reports EVERY command the user's Claude install exposes — including
351
+ // all their unrelated skills/plugins (Cloudflare, codex:*, etc.). Surface only
352
+ // the built-ins that make sense inside the ComfyUI panel chat.
353
+ const PANEL_SLASH_ALLOWLIST = new Set(["compact", "context", "usage", "loop", "goal", "clear"]);
354
+ function pushCommands(tabId) {
355
+ void ensureCommands()
356
+ .then((commands) => {
357
+ const useful = commands.filter((c) => PANEL_SLASH_ALLOWLIST.has(c.name));
358
+ if (useful.length)
359
+ bridge.push({ type: "commands", commands: useful }, tabId);
360
+ })
361
+ .catch(() => {
362
+ /* probe already logged; panel just won't show SDK commands */
363
+ });
364
+ }
99
365
  bridge.onPanelMessage = (event) => {
366
+ // Connect ack: the instant a panel tab connects, the orchestrator announces
367
+ // itself so "connected" means "a real agent is attending" — not merely "a
368
+ // socket is open." A bare/undriven bridge stays silent, so the panel can
369
+ // tell the difference (and warn if no ack arrives).
370
+ if (event.type === "hello" && event.tab_id) {
371
+ // Reload restore: the panel re-sends the last session id it saw so the
372
+ // agent's memory continues. Only honored before the tab's agent spawns.
373
+ const resume = typeof event.resume === "string" ? event.resume : undefined;
374
+ if (resume)
375
+ manager.setResume(event.tab_id, resume);
376
+ // Send the live model list so the picker reflects the real subscription,
377
+ // and the SDK's slash commands so the composer can surface them.
378
+ pushModels(event.tab_id);
379
+ pushCommands(event.tab_id);
380
+ // Re-push the last usage so the context meter isn't blank after a reload.
381
+ const lastStatus = manager.lastStatusFor(event.tab_id);
382
+ if (lastStatus)
383
+ pushStatus(event.tab_id, lastStatus);
384
+ const tabId = event.tab_id;
385
+ const now = Date.now();
386
+ if (now - (lastAckAt.get(tabId) ?? 0) < ACK_DEBOUNCE_MS)
387
+ return;
388
+ lastAckAt.set(tabId, now);
389
+ // TRUTHFUL "connected": only claim ready after PROVING the SDK can run, by
390
+ // probing the model list (same machinery the agent uses to spawn). If the
391
+ // probe fails — the "connected but dead" wedge — say so and send a degraded
392
+ // ack instead of a green ready, so the panel can show the real state.
393
+ void ensureModels()
394
+ .then((models) => {
395
+ if (models.length) {
396
+ // Greet only on a FRESH session. On a reconnect/resume — a panel swap,
397
+ // a WS blip, or a real restart (all carry `resume`) — the user already
398
+ // has their thread, so re-greeting is just noise. The ack still fires.
399
+ if (!resume) {
400
+ bridge.push({ type: "say", text: `🟢 comfyui-mcp agent ready — ${model} on your Claude subscription. Ask away.` }, tabId);
401
+ }
402
+ bridge.push({ type: "ack", ok: true, kind: "ready", agent: model }, tabId);
403
+ logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} connected — agent healthy, sent ready ack`);
404
+ }
405
+ else {
406
+ bridge.push({
407
+ type: "say",
408
+ text: "⚠️ The background agent isn't responding — the Claude Agent SDK couldn't start. Make sure you're signed in (run `claude` once), then Disconnect → Connect to retry.",
409
+ }, tabId);
410
+ bridge.push({ type: "ack", ok: false, kind: "degraded" }, tabId);
411
+ logger.warn(`[panel-orchestrator] tab ${tabId.slice(0, 8)} connected but model probe empty — sent degraded ack`);
412
+ }
413
+ })
414
+ .catch(() => {
415
+ bridge.push({ type: "ack", ok: false, kind: "degraded" }, tabId);
416
+ });
417
+ return;
418
+ }
419
+ // Model / effort picker: apply and confirm. Model switches live; an effort
420
+ // change restarts the session (resumed) so the conversation carries over.
421
+ if (event.type === "set_options" && event.tab_id) {
422
+ const tabId = event.tab_id;
423
+ const reqModel = typeof event.model === "string" ? event.model : undefined;
424
+ const nextEffort = event.effort === null
425
+ ? null
426
+ : isEffort(event.effort)
427
+ ? event.effort
428
+ : undefined;
429
+ void (async () => {
430
+ let nextModel = reqModel;
431
+ // Guard: never switch to a model the account can't use — an unknown id
432
+ // makes the SDK session hang on init. (Defense in depth; the panel only
433
+ // sends ids from the live catalog.)
434
+ if (nextModel) {
435
+ const known = await ensureModels().catch(() => []);
436
+ if (known.length && !known.some((m) => m.value === nextModel)) {
437
+ logger.warn(`[panel-orchestrator] ignoring unknown model "${nextModel}" — keeping current`);
438
+ nextModel = undefined;
439
+ }
440
+ }
441
+ const applied = await manager.setOptions(tabId, { model: nextModel, effort: nextEffort });
442
+ bridge.push({
443
+ type: "ack",
444
+ ok: true,
445
+ kind: "options",
446
+ model: applied.model,
447
+ effort: applied.effort ?? null,
448
+ restarted: applied.restarted,
449
+ // Effort changed mid-turn → it takes effect once the current turn ends
450
+ // (we never interrupt a live reply). The panel can note this.
451
+ deferred: applied.deferred,
452
+ }, tabId);
453
+ })().catch((err) => {
454
+ bridge.push({ type: "say", text: `⚠️ Could not change model/effort: ${err?.message ?? err}` }, tabId);
455
+ });
456
+ return;
457
+ }
458
+ // Execution event from the panel (run finished / errored). Feed it to the
459
+ // tab's live agent so it knows its render landed and can comment/iterate.
460
+ // Dropped silently if no agent is attending the tab (we don't spawn one).
461
+ if (event.type === "agent_event" && event.tab_id) {
462
+ const delivered = manager.injectEvent(event.tab_id, event);
463
+ if (delivered) {
464
+ logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} event → agent: ${event.kind}`);
465
+ }
466
+ return;
467
+ }
468
+ // Interrupt: stop the current turn without ending the session (Ctrl+C in
469
+ // the panel). The session stays open for the next message.
470
+ if (event.type === "interrupt" && event.tab_id) {
471
+ const tabId = event.tab_id;
472
+ void manager.interrupt(tabId);
473
+ bridge.push({ type: "ack", ok: true, kind: "interrupt" }, tabId);
474
+ logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} interrupted`);
475
+ return;
476
+ }
477
+ // The user edited/deleted a still-QUEUED message before the agent read it —
478
+ // drop it from the agent's queue so it's never processed.
479
+ if (event.type === "cancel_message" && event.tab_id) {
480
+ const tabId = event.tab_id;
481
+ const mid = typeof event.mid === "string" ? event.mid : undefined;
482
+ const removed = mid ? manager.cancelQueued(tabId, mid) : false;
483
+ bridge.push({ type: "ack", ok: true, kind: "cancel_message", mid, removed }, tabId);
484
+ return;
485
+ }
486
+ // New chat: forget this tab's session so the next message starts fresh (no
487
+ // memory of the prior conversation). Tell the panel to drop its stored id.
488
+ if (event.type === "new_session" && event.tab_id) {
489
+ const tabId = event.tab_id;
490
+ // reset() is synchronous (map cleared now), so no concurrent send() can
491
+ // spawn an agent before we report the cleared session.
492
+ manager.reset(tabId);
493
+ bridge.push({ type: "session", session_id: null }, tabId);
494
+ bridge.push({ type: "ack", ok: true, kind: "new_session" }, tabId);
495
+ return;
496
+ }
497
+ // Rewind the conversation: fork the live session at `anchor` (an assistant
498
+ // UUID the panel stored from onTurnAnchor) so everything after it is dropped,
499
+ // optionally continuing with the user's edited `text`. The panel handles the
500
+ // graph (code) scope locally; this is the conversation scope.
501
+ if (event.type === "rewind" && event.tab_id) {
502
+ const tabId = event.tab_id;
503
+ const anchor = typeof event.anchor === "string" ? event.anchor : null;
504
+ const ok = manager.rewind(tabId, anchor);
505
+ bridge.push({ type: "ack", ok, kind: "rewind" }, tabId);
506
+ logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} rewind (anchor=${anchor ? anchor.slice(0, 8) : "fresh"}, ok=${ok})`);
507
+ return;
508
+ }
509
+ // Reorder still-queued messages to the panel's desired flush order.
510
+ if (event.type === "reorder" && event.tab_id) {
511
+ const tabId = event.tab_id;
512
+ const order = Array.isArray(event.order)
513
+ ? event.order.filter((m) => typeof m === "string")
514
+ : [];
515
+ const ok = manager.reorderQueue(tabId, order);
516
+ bridge.push({ type: "ack", ok, kind: "reorder" }, tabId);
517
+ logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} reorder queue (${order.length} mids, ok=${ok})`);
518
+ return;
519
+ }
520
+ // Switch to a historical chat: drop the live agent and arm a resume so the
521
+ // next message continues THAT conversation. Both calls are synchronous, so
522
+ // the resume is armed before any later message can spawn a fresh agent.
523
+ if (event.type === "resume_session" && event.tab_id) {
524
+ const tabId = event.tab_id;
525
+ const sid = typeof event.session_id === "string" ? event.session_id : undefined;
526
+ manager.reset(tabId);
527
+ if (sid)
528
+ manager.setResume(tabId, sid);
529
+ bridge.push({ type: "ack", ok: true, kind: "resume_session" }, tabId);
530
+ return;
531
+ }
100
532
  if (event.type !== "user_message" ||
101
533
  typeof event.text !== "string" ||
102
534
  !event.tab_id) {
@@ -104,18 +536,106 @@ export async function runPanelOrchestrator() {
104
536
  }
105
537
  // Echo so the user immediately sees their own message land in the chat.
106
538
  bridge.push({ type: "echo", text: event.text }, event.tab_id);
539
+ // Per-message ack: a live server-side signal that the agent received this
540
+ // turn and is working — distinct from the panel's own optimistic spinner.
541
+ // Echo the client mid so the panel can mark that exact bubble delivered.
542
+ const userMid = typeof event.mid === "string" ? event.mid : undefined;
543
+ bridge.push({ type: "ack", ok: true, kind: "working", ...(userMid ? { mid: userMid } : {}) }, event.tab_id);
544
+ // Show the working indicator immediately (before the first assistant token).
545
+ bridge.push({ type: "turn", state: "working" }, event.tab_id);
107
546
  logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} → agent: ${event.text.slice(0, 80)}`);
108
- manager.send(event.tab_id, event.text, { title: event.title });
547
+ manager.send(event.tab_id, event.text, {
548
+ title: event.title,
549
+ images: event.images,
550
+ mid: userMid,
551
+ });
109
552
  };
110
- logger.info(`[panel-orchestrator] ready bridge on ws://127.0.0.1:9101; an agent spawns per ComfyUI tab on its first message (model=${model}, comfyui=${comfyuiUrl})`);
553
+ // ---- Download-progress watcher ----
554
+ // Each tab's comfyui MCP (download_model) writes per-download JSON into
555
+ // progressDir; poll it and broadcast the rows to every panel tab's tray.
556
+ // Done/error rows linger briefly (so completion is visible), then are pruned;
557
+ // a downloading row that stops updating for 60s is treated as a dead writer.
558
+ const DOWNLOAD_LINGER_MS = 8000;
559
+ const downloadRemoveAt = new Map();
560
+ let lastDownloadSnapshot = "[]";
561
+ const pollDownloads = () => {
562
+ let files = [];
563
+ try {
564
+ files = readdirSync(progressDir).filter((f) => f.endsWith(".json"));
565
+ }
566
+ catch {
567
+ files = []; // dir not created yet — nothing downloading
568
+ }
569
+ const now = Date.now();
570
+ const downloads = [];
571
+ for (const f of files) {
572
+ const full = join(progressDir, f);
573
+ let row;
574
+ try {
575
+ row = JSON.parse(readFileSync(full, "utf8"));
576
+ }
577
+ catch {
578
+ continue; // mid-write or corrupt — retry next tick
579
+ }
580
+ if (!row || typeof row !== "object")
581
+ continue;
582
+ const status = row.status;
583
+ const updated = typeof row.updated === "number" ? row.updated : now;
584
+ if (status === "done" || status === "error") {
585
+ const due = downloadRemoveAt.get(full);
586
+ if (due == null) {
587
+ downloadRemoveAt.set(full, now + DOWNLOAD_LINGER_MS); // start the linger
588
+ }
589
+ else if (now >= due) {
590
+ try {
591
+ unlinkSync(full);
592
+ }
593
+ catch { /* already gone */ }
594
+ downloadRemoveAt.delete(full);
595
+ continue; // pruned from the tray
596
+ }
597
+ }
598
+ else {
599
+ downloadRemoveAt.delete(full);
600
+ if (now - updated > 60000) {
601
+ try {
602
+ unlinkSync(full);
603
+ }
604
+ catch { /* ignore */ }
605
+ continue; // dead writer (crashed mid-download)
606
+ }
607
+ }
608
+ downloads.push(row);
609
+ }
610
+ downloads.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? "")));
611
+ const snapshot = JSON.stringify(downloads);
612
+ if (snapshot !== lastDownloadSnapshot) {
613
+ lastDownloadSnapshot = snapshot;
614
+ bridge.push({ type: "download_progress", downloads }); // broadcast to all tabs
615
+ }
616
+ };
617
+ const downloadTimer = setInterval(pollDownloads, 700);
618
+ downloadTimer.unref?.();
619
+ logger.info(`[panel-orchestrator] ready — bridge on ws://127.0.0.1:${bridgePort}; an agent spawns per ComfyUI tab on its first message (model=${model}, comfyui=${comfyuiUrl}${comfyuiPath ? `, path=${comfyuiPath}` : " — no COMFYUI_PATH, local install/pack tools limited"})`);
111
620
  let shuttingDown = false;
112
621
  const shutdown = async () => {
113
622
  if (shuttingDown)
114
623
  return;
115
624
  shuttingDown = true;
116
625
  logger.info("[panel-orchestrator] shutting down — stopping agents…");
626
+ clearInterval(downloadTimer);
117
627
  await manager.stopAll();
118
628
  await bridge.stop();
629
+ // Only remove the lockfile if it still names us — avoid clobbering a fresh
630
+ // orchestrator that may have replaced us.
631
+ try {
632
+ const cur = JSON.parse(readFileSync(lockPath, "utf8"));
633
+ if (cur?.pid === process.pid)
634
+ unlinkSync(lockPath);
635
+ }
636
+ catch {
637
+ // No lockfile / unreadable — nothing to clean up.
638
+ }
119
639
  process.exit(0);
120
640
  };
121
641
  process.on("SIGINT", shutdown);