comfyui-mcp 0.27.0 → 0.28.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.
@@ -17,6 +17,7 @@ const CLI_NAMES = {
17
17
  codex: ["codex", "codex.cmd", "codex.exe"],
18
18
  gemini: ["gemini", "gemini.cmd", "gemini.exe"],
19
19
  ollama: ["ollama", "ollama.exe"],
20
+ lmstudio: ["lms", "lms.exe"],
20
21
  };
21
22
  /** Well-known Ollama install locations probed in addition to PATH (the Windows
22
23
  * installer adds PATH for NEW shells only — an orchestrator started from an
@@ -30,6 +31,32 @@ function ollamaInstalled(home) {
30
31
  }
31
32
  return fileExists("/usr/local/bin/ollama") || fileExists("/opt/homebrew/bin/ollama");
32
33
  }
34
+ /** LM Studio install signals: the app drops its `lms` CLI at ~/.lmstudio/bin on
35
+ * every platform (best cross-OS marker), plus the per-OS app locations. */
36
+ function lmstudioInstalled(home) {
37
+ if (lmstudioCliPath(home))
38
+ return true;
39
+ if (process.platform === "win32") {
40
+ const localAppData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
41
+ return fileExists(localAppData, "Programs", "LM Studio", "LM Studio.exe");
42
+ }
43
+ if (process.platform === "darwin")
44
+ return fileExists("/Applications", "LM Studio.app");
45
+ return fileExists(home, ".lmstudio");
46
+ }
47
+ /** Absolute path (or bare name if on PATH) of the `lms` CLI — the supported
48
+ * programmatic surface for load/unload/server lifecycle. Used by
49
+ * services/lmstudio-lifecycle to shell out even when PATH lacks it (the app
50
+ * adds ~/.lmstudio/bin to PATH for NEW shells only). Null when not found. */
51
+ export function lmstudioCliPath(home = homedir()) {
52
+ if (onPath(CLI_NAMES.lmstudio))
53
+ return "lms";
54
+ const binName = process.platform === "win32" ? "lms.exe" : "lms";
55
+ const inHome = join(home, ".lmstudio", "bin", binName);
56
+ if (fileExists(inHome))
57
+ return inHome;
58
+ return null;
59
+ }
33
60
  /** True if any of `names` resolves on the local PATH. */
34
61
  function onPath(names) {
35
62
  const sep = process.platform === "win32" ? ";" : ":";
@@ -90,6 +117,13 @@ export function backendReadiness(backend, opts) {
90
117
  const cli = ollamaInstalled(home);
91
118
  return { backend: "ollama", cli, auth: cli ? true : null, ready: cli };
92
119
  }
120
+ if (b === "lmstudio") {
121
+ // Same posture as ollama: a local server, no login concept. App/CLI
122
+ // presence is the readiness signal; a stopped server still surfaces via
123
+ // the connect ack's model probe (GET /v1/models fails → degraded ack).
124
+ const cli = lmstudioInstalled(home);
125
+ return { backend: "lmstudio", cli, auth: cli ? true : null, ready: cli };
126
+ }
93
127
  if (b === "openrouter") {
94
128
  // Hosted — no CLI. Readiness = an OpenRouter API key in the orchestrator's
95
129
  // env (OPENROUTER_API_KEY, or the shared COMFYUI_MCP_OLLAMA_API_KEY). A bad
@@ -1 +1 @@
1
- {"version":3,"file":"backend-readiness.js","sourceRoot":"","sources":["../../src/orchestrator/backend-readiness.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,qEAAqE;AACrE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,+EAA+E;AAC/E,iFAAiF;AACjF,kFAAkF;AAClF,2EAA2E;AAC3E,mCAAmC;AAEnC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAYjC,gFAAgF;AAChF,qEAAqE;AACrE,MAAM,SAAS,GAA6B;IAC1C,KAAK,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,CAAC;IAC1C,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,YAAY,CAAC;IAC9C,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC;CACjC,CAAC;AAEF;;qDAEqD;AACrD,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QAChF,OAAO,UAAU,CAAC,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,UAAU,CAAC,uBAAuB,CAAC,IAAI,UAAU,CAAC,0BAA0B,CAAC,CAAC;AACvF,CAAC;AAED,yDAAyD;AACzD,SAAS,MAAM,CAAC,KAAe;IAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACrD,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjE,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;oBAAE,OAAO,IAAI,CAAC;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACP,+BAA+B;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CAAC,GAAG,KAAe;IACpC,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,IAAwB;IACxE,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,EAAE,IAAI,IAAI,OAAO,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;QAClB,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;QACrD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7D,CAAC;IACD,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACrC,4EAA4E;QAC5E,iCAAiC;QACjC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC;QACvD,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,CAAC,CAAC;QACnE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC;IAC9D,CAAC;IACD,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnB,sEAAsE;QACtE,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IACzE,CAAC;IACD,IAAI,CAAC,KAAK,YAAY,EAAE,CAAC;QACvB,2EAA2E;QAC3E,4EAA4E;QAC5E,mEAAmE;QACnE,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QACzF,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IACnF,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC/D,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,mBAAmB,CACjC,QAA0B,EAC1B,IAAwB;IAKxB,MAAM,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IACjE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;AAClE,CAAC"}
1
+ {"version":3,"file":"backend-readiness.js","sourceRoot":"","sources":["../../src/orchestrator/backend-readiness.ts"],"names":[],"mappings":"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"}
@@ -33,6 +33,7 @@ import { startPanelMcpHttpServer } from "./panel-mcp-http.js";
33
33
  import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
34
34
  import { QueueMonitor } from "../services/queue-monitor.js";
35
35
  import { unloadAllOllama, warmOllama, resolveOllamaHost } from "../services/ollama-vram.js";
36
+ import { isLocalLmstudio, startLmstudioServer, unloadAllLmstudio, warmLmstudio, } from "../services/lmstudio-lifecycle.js";
36
37
  import { getAgentSettings, setAgentSettings } from "../services/panel-settings.js";
37
38
  import { gatherEnvCapabilities, buildPanelSystemAppend, } from "../services/env-capabilities.js";
38
39
  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.
@@ -119,6 +120,21 @@ const injectedCrashes = new Set();
119
120
  * doesn't prepend the same warning to every message. A new running prompt id (or
120
121
  * a fresh backlog) produces a new key and warns again. Process-scoped. */
121
122
  const injectedQueueNotes = new Set();
123
+ /** HEADLESS clients (the mobile / remote pseudo-panel) connect with NO ComfyUI
124
+ * browser panel. Two consequences: the live-canvas panel_* tools can't run, AND
125
+ * — critically — nothing observes a render finishing to auto-deliver its output
126
+ * back to the agent (that path is browser-driven; see the `agent_event` handler).
127
+ * generate_image / enqueue_workflow return a prompt_id immediately, so a headless
128
+ * agent that "ends its turn and waits" never surfaces the image. This directive,
129
+ * prepended to each headless-tab turn like the crash/queue notes, tells it to run
130
+ * headless and deliver the result ITSELF, in-turn. */
131
+ const HEADLESS_DIRECTIVE = "[HEADLESS SESSION — no ComfyUI panel/canvas is connected] The panel_* live-canvas tools " +
132
+ "(panel_run, panel_get_graph, panel_set_widget, panel_add_node, …) are UNAVAILABLE here and will fail — " +
133
+ "do everything through the comfyui MCP tools (generate_image, or create_workflow + enqueue_workflow). " +
134
+ "There is NO panel to auto-deliver a finished render, so you MUST deliver the result YOURSELF IN THIS SAME TURN: " +
135
+ "enqueuing returns a prompt_id immediately, so wait for it with get_job_status(prompt_id) — poll it briefly until " +
136
+ "it reports completion (this is the ONE case where polling IS correct) — then fetch the output with get_history and " +
137
+ "show it with panel_show_media. Do NOT end your turn expecting an automatic notification; none will arrive.";
122
138
  /** Live stall threshold (seconds) pushed from the panel setting via a `set_config`
123
139
  * frame — applies WITHOUT a reconnect. null = not set, fall back to env then the
124
140
  * built-in default. Process-global: one ComfyUI per orchestrator. */
@@ -830,6 +846,13 @@ export async function runPanelOrchestrator() {
830
846
  ...(key ? { apiKey: key } : {}),
831
847
  };
832
848
  };
849
+ // LM Studio (issue #160) = the same openai dialect pinned to LM Studio's
850
+ // local server. No API key, no login. Default model is EMPTY on purpose —
851
+ // LM Studio model ids are user-specific, so ensureModels() below fills it
852
+ // with the first id the server actually offers.
853
+ const LMSTUDIO_BASE_URL = (process.env.COMFYUI_MCP_LMSTUDIO_HOST ?? "http://127.0.0.1:1234/v1").replace(/[/]$/, "");
854
+ let lmstudioModel = process.env.COMFYUI_MCP_LMSTUDIO_MODEL ?? persistedAgent.lmstudio?.model ?? "";
855
+ const lmstudioDeps = () => ({ api: "openai", host: LMSTUDIO_BASE_URL });
833
856
  // ── Per-tab backend (single-port multi-provider) ──────────────────────────
834
857
  // ONE orchestrator on ONE bridge port serves ALL providers; the panel picks a
835
858
  // provider per tab via the `hello`/`set_backend` handshake, instead of the node
@@ -839,10 +862,11 @@ export async function runPanelOrchestrator() {
839
862
  // provider (the panel replays the transcript to seed it) while a same-provider
840
863
  // reconnect RESUMES. `backendId`/`codexModel`/`geminiModel` above are the
841
864
  // DEFAULT + per-provider model config; the process is no longer pinned to one.
842
- const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini", "ollama", "openrouter"]);
865
+ const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini", "ollama", "openrouter", "lmstudio"]);
843
866
  const defaultBackend = KNOWN_BACKENDS.has(backendId) ? backendId : "claude";
844
867
  const AGENT_KEY_SEP = "::";
845
868
  const tabBackends = new Map(); // panel tabId -> selected backend
869
+ const headlessTabs = new Set(); // tabs with no ComfyUI canvas (mobile/remote) — deliver renders in-turn
846
870
  const backendForTab = (panelTabId) => tabBackends.get(panelTabId) ?? defaultBackend;
847
871
  const agentKeyFor = (panelTabId) => panelTabId + AGENT_KEY_SEP + backendForTab(panelTabId);
848
872
  // A panel tab id never contains "::"; backend names never do — so split on the
@@ -996,6 +1020,16 @@ export async function runPanelOrchestrator() {
996
1020
  ...openrouterDeps(),
997
1021
  });
998
1022
  }
1023
+ if (backend === "lmstudio") {
1024
+ return new OllamaBackend({
1025
+ cwd: comfyuiPath ?? process.cwd(),
1026
+ model: lmstudioModel,
1027
+ systemAppend: panelSystemAppend,
1028
+ comfyuiUrl,
1029
+ mcpServers: makeHttpBackendMcpServers(panelTabId),
1030
+ ...lmstudioDeps(),
1031
+ });
1032
+ }
999
1033
  return undefined; // claude → built-in ClaudeBackend
1000
1034
  };
1001
1035
  logger.info(`[panel-orchestrator] single-port multi-provider: default backend=${defaultBackend}; ` +
@@ -1018,7 +1052,9 @@ export async function runPanelOrchestrator() {
1018
1052
  ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: ollamaModel, ...ollamaDeps() })
1019
1053
  : backend === "openrouter"
1020
1054
  ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: openrouterModel, ...openrouterDeps() })
1021
- : new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
1055
+ : backend === "lmstudio"
1056
+ ? new OllamaBackend({ cwd: comfyuiPath ?? process.cwd(), model: lmstudioModel, ...lmstudioDeps() })
1057
+ : new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
1022
1058
  probeBackends.set(backend, pb);
1023
1059
  }
1024
1060
  return pb;
@@ -1123,17 +1159,26 @@ export async function runPanelOrchestrator() {
1123
1159
  const pauseLocalDuringGen = process.env.COMFYUI_MCP_OLLAMA_PAUSE_ON_GEN !== "0";
1124
1160
  const anyLocalOllama = () => ollamaApi === "ollama" &&
1125
1161
  (defaultBackend === "ollama" || [...tabBackends.values()].includes("ollama"));
1162
+ // LM Studio joins the same VRAM handoff (issue #160 follow-up): local server
1163
+ // only — a remote COMFYUI_MCP_LMSTUDIO_HOST is someone else's VRAM.
1164
+ const anyLocalLmstudio = () => isLocalLmstudio(LMSTUDIO_BASE_URL) &&
1165
+ (defaultBackend === "lmstudio" || [...tabBackends.values()].includes("lmstudio"));
1126
1166
  // agentKey -> messages held while a render runs (flushed on render end).
1127
1167
  const heldDuringGen = new Map();
1128
1168
  let genPauseActive = false;
1129
1169
  if (pauseLocalDuringGen) {
1130
1170
  QueueMonitor.setTransitionHandlers({
1131
1171
  onRunStart: () => {
1132
- if (!anyLocalOllama())
1172
+ const ol = anyLocalOllama();
1173
+ const ls = anyLocalLmstudio();
1174
+ if (!ol && !ls)
1133
1175
  return;
1134
1176
  genPauseActive = true;
1135
1177
  // Free the local model's VRAM for the render (best-effort, fire-and-forget).
1136
- void unloadAllOllama(resolveOllamaHost());
1178
+ if (ol)
1179
+ void unloadAllOllama(resolveOllamaHost());
1180
+ if (ls)
1181
+ void unloadAllLmstudio(LMSTUDIO_BASE_URL);
1137
1182
  },
1138
1183
  onRunEnd: () => {
1139
1184
  if (!genPauseActive)
@@ -1145,6 +1190,8 @@ export async function runPanelOrchestrator() {
1145
1190
  // loads the model itself — no separate warm needed.
1146
1191
  if (anyLocalOllama() && !hadHeld)
1147
1192
  void warmOllama(resolveOllamaHost(), ollamaModel);
1193
+ if (anyLocalLmstudio() && !hadHeld && lmstudioModel)
1194
+ void warmLmstudio(LMSTUDIO_BASE_URL, lmstudioModel);
1148
1195
  for (const [key, msgs] of heldDuringGen) {
1149
1196
  const tabId = panelTabOf(key);
1150
1197
  if (msgs.length > 0) {
@@ -1275,6 +1322,14 @@ export async function runPanelOrchestrator() {
1275
1322
  p = probe.then((list) => {
1276
1323
  if (!list.length)
1277
1324
  modelsByBackend.delete(backend); // don't cache a failed probe
1325
+ // LM Studio ships no sane hardcoded default (model ids are whatever the
1326
+ // user downloaded) — adopt the server's first offering when unset.
1327
+ if (backend === "lmstudio" && !lmstudioModel && list.length) {
1328
+ lmstudioModel = list[0].value ?? "";
1329
+ if (lmstudioModel) {
1330
+ logger.info(`[panel-orchestrator] lmstudio default model → ${lmstudioModel} (first served)`);
1331
+ }
1332
+ }
1278
1333
  // User-curated preferred models (panel Settings → set_config) pin to the
1279
1334
  // top of the ollama picker, ahead of the discovered catalog. Read fresh
1280
1335
  // on every probe; set_config evicts the cache so edits apply live.
@@ -1309,6 +1364,8 @@ export async function runPanelOrchestrator() {
1309
1364
  return ollamaModel;
1310
1365
  if (backend === "openrouter")
1311
1366
  return openrouterModel;
1367
+ if (backend === "lmstudio")
1368
+ return lmstudioModel || undefined;
1312
1369
  return model;
1313
1370
  }
1314
1371
  function pushModels(panelTabId) {
@@ -1405,6 +1462,12 @@ export async function runPanelOrchestrator() {
1405
1462
  manager.reset(panelTab + AGENT_KEY_SEP + prev);
1406
1463
  }
1407
1464
  tabBackends.set(panelTab, backend);
1465
+ // A headless client (mobile/remote pseudo-panel, no browser canvas) advertises
1466
+ // itself in the hello frame so its agent gets the in-turn-delivery directive.
1467
+ if (event.headless === true)
1468
+ headlessTabs.add(panelTab);
1469
+ else
1470
+ headlessTabs.delete(panelTab);
1408
1471
  const key = panelTab + AGENT_KEY_SEP + backend;
1409
1472
  // Reload restore: the panel re-sends the last session id it saw. Honored
1410
1473
  // only for a SAME-provider (re)connect — a switch always starts fresh. The
@@ -1431,6 +1494,7 @@ export async function runPanelOrchestrator() {
1431
1494
  const isGm = backend === "gemini";
1432
1495
  const isOl = backend === "ollama";
1433
1496
  const isOr = backend === "openrouter";
1497
+ const isLs = backend === "lmstudio";
1434
1498
  // TRUTHFUL "connected": only claim ready after PROVING the SELECTED backend
1435
1499
  // can run, by probing its model list. If the probe fails — the "connected
1436
1500
  // but dead" wedge — send a degraded ack so the panel shows the real state.
@@ -1448,7 +1512,9 @@ export async function runPanelOrchestrator() {
1448
1512
  logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (openrouter) but no API key — degraded ack`);
1449
1513
  return;
1450
1514
  }
1451
- void ensureModels(backend)
1515
+ void (isLs && isLocalLmstudio(LMSTUDIO_BASE_URL)
1516
+ ? startLmstudioServer(LMSTUDIO_BASE_URL).then(() => ensureModels(backend))
1517
+ : ensureModels(backend))
1452
1518
  .then((models) => {
1453
1519
  if (models.length) {
1454
1520
  const agentLabel = isCx
@@ -1457,9 +1523,11 @@ export async function runPanelOrchestrator() {
1457
1523
  ? (geminiModel ?? models[0].value ?? "Gemini")
1458
1524
  : isOl
1459
1525
  ? (ollamaModel ?? models[0].value ?? "Ollama")
1460
- : isOr
1461
- ? (openrouterModel ?? models[0].value ?? "OpenRouter")
1462
- : model;
1526
+ : isLs
1527
+ ? (lmstudioModel || (models[0].value ?? "LM Studio"))
1528
+ : isOr
1529
+ ? (openrouterModel ?? models[0].value ?? "OpenRouter")
1530
+ : model;
1463
1531
  // Greet only on a FRESH session (a resume/reconnect already has the thread).
1464
1532
  if (!resume) {
1465
1533
  const readyText = isCx
@@ -1468,9 +1536,11 @@ export async function runPanelOrchestrator() {
1468
1536
  ? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
1469
1537
  : isOl
1470
1538
  ? `🟢 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.`
1471
- : isOr
1472
- ? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
1473
- : `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
1539
+ : isLs
1540
+ ? `🟢 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.`
1541
+ : isOr
1542
+ ? `🟢 comfyui-mcp agent ready — ${agentLabel} via OpenRouter (hosted API, your OPENROUTER_API_KEY). Ask away.`
1543
+ : `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
1474
1544
  bridge.push({ type: "say", text: readyText }, panelTab);
1475
1545
  }
1476
1546
  bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
@@ -1483,7 +1553,9 @@ export async function runPanelOrchestrator() {
1483
1553
  ? "⚠️ The background agent isn't responding — the Gemini CLI couldn't start. Make sure the Gemini CLI is installed and signed in (run `gemini` once and complete the Google sign-in), then Disconnect → Connect to retry."
1484
1554
  : isOl
1485
1555
  ? "⚠️ The background agent isn't responding — Ollama isn't reachable. Start it with `ollama serve` and pull our fine-tuned model (`ollama pull artokun/gemma4-comfyui-mcp:e4b` — gemma4 trained on the comfyui-mcp tool suite; `:e2b` for ~2 GB VRAM, `:12b` for ~8 GB), then Disconnect → Connect to retry."
1486
- : "⚠️ 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.";
1556
+ : isLs
1557
+ ? `⚠️ The background agent isn't responding — LM Studio isn't reachable at ${LMSTUDIO_BASE_URL}. Open LM Studio → Developer → Start Server and load a tool-calling model (our gemma4-comfyui-mcp GGUFs from Hugging Face work great), or set COMFYUI_MCP_LMSTUDIO_HOST if it serves elsewhere — then Disconnect → Connect to retry.`
1558
+ : "⚠️ 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.";
1487
1559
  bridge.push({ type: "say", text: degradedText }, panelTab);
1488
1560
  bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
1489
1561
  logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) but model probe empty — degraded ack`);
@@ -1511,6 +1583,21 @@ export async function runPanelOrchestrator() {
1511
1583
  if (prev !== reqBackend)
1512
1584
  manager.reset(panelTab + AGENT_KEY_SEP + prev);
1513
1585
  tabBackends.set(panelTab, reqBackend);
1586
+ // Leaving a LOCAL provider frees its VRAM (no other tab still on it) —
1587
+ // the point of switching to Claude/hosted is usually reclaiming the GPU.
1588
+ if (prev !== reqBackend) {
1589
+ const stillUsed = (b) => [...tabBackends.values()].includes(b) || defaultBackend === b;
1590
+ if (prev === "lmstudio" && !stillUsed("lmstudio") && isLocalLmstudio(LMSTUDIO_BASE_URL)) {
1591
+ void unloadAllLmstudio(LMSTUDIO_BASE_URL);
1592
+ }
1593
+ if (prev === "ollama" && !stillUsed("ollama") && ollamaApi === "ollama") {
1594
+ void unloadAllOllama(resolveOllamaHost());
1595
+ }
1596
+ // Switching TO lmstudio: make sure its server is up (auto-start, no
1597
+ // manual `lms server start`) so the readiness probe finds it alive.
1598
+ if (reqBackend === "lmstudio")
1599
+ void startLmstudioServer(LMSTUDIO_BASE_URL);
1600
+ }
1514
1601
  pushModels(panelTab);
1515
1602
  if (reqBackend === "claude")
1516
1603
  pushCommands(panelTab);
@@ -1567,6 +1654,22 @@ export async function runPanelOrchestrator() {
1567
1654
  logger.info(`[panel-orchestrator] ollama config → model=${ollamaModel} api=${ollamaApi} host=${ollamaBaseUrl ?? "(default)"}`);
1568
1655
  }
1569
1656
  }
1657
+ const lcfg = event.lmstudio;
1658
+ if (lcfg && typeof lcfg === "object") {
1659
+ const o = lcfg;
1660
+ if (typeof o.model === "string" && o.model.trim()) {
1661
+ const m = o.model.trim();
1662
+ if (!process.env.COMFYUI_MCP_LMSTUDIO_MODEL)
1663
+ lmstudioModel = m;
1664
+ setAgentSettings({ lmstudio: { model: m } });
1665
+ const pb = probeBackends.get("lmstudio");
1666
+ if (pb?.close)
1667
+ void pb.close().catch(() => { });
1668
+ probeBackends.delete("lmstudio");
1669
+ modelsByBackend.delete("lmstudio");
1670
+ logger.info(`[panel-orchestrator] lmstudio config → model=${lmstudioModel}`);
1671
+ }
1672
+ }
1570
1673
  if (ollamaChanged) {
1571
1674
  modelsByBackend.delete("ollama");
1572
1675
  pushModels(event.tab_id);
@@ -1623,6 +1726,12 @@ export async function runPanelOrchestrator() {
1623
1726
  nextModel = undefined;
1624
1727
  }
1625
1728
  }
1729
+ // LM Studio model switch: unload everything EXCEPT the incoming model —
1730
+ // the outgoing one would otherwise sit in VRAM next to the JIT-loaded
1731
+ // replacement until its TTL expires.
1732
+ if (nextModel && backendForTab(tabId) === "lmstudio" && isLocalLmstudio(LMSTUDIO_BASE_URL)) {
1733
+ void unloadAllLmstudio(LMSTUDIO_BASE_URL, nextModel);
1734
+ }
1626
1735
  const applied = await manager.setOptions(agentKeyFor(tabId), { model: nextModel, effort: nextEffort });
1627
1736
  bridge.push({
1628
1737
  type: "ack",
@@ -1788,6 +1897,14 @@ export async function runPanelOrchestrator() {
1788
1897
  catch (err) {
1789
1898
  logger.debug(`[panel-orchestrator] queue-note check failed (ignored): ${err instanceof Error ? err.message : String(err)}`);
1790
1899
  }
1900
+ // HEADLESS delivery: a mobile/remote tab has no browser panel to auto-deliver a
1901
+ // finished render, so remind its agent — every turn, since it must hold for the
1902
+ // whole session — to run headless and show the output itself in-turn. The note is
1903
+ // short and always-correct, so (unlike the once-per-episode crash/queue notes) it
1904
+ // is injected on every headless turn.
1905
+ if (headlessTabs.has(event.tab_id)) {
1906
+ outText = `${HEADLESS_DIRECTIVE}\n\n${outText}`;
1907
+ }
1791
1908
  // Transcript replay (single-port provider switch): the panel sends the prior
1792
1909
  // conversation as `context` on the FIRST message to a freshly-switched
1793
1910
  // provider, so the new backend has the thread (minus internal session data —
@@ -1807,10 +1924,9 @@ export async function runPanelOrchestrator() {
1807
1924
  // render is in flight, DON'T run the turn now — that would reload the model
1808
1925
  // on top of the generation (VRAM contention / OOM). Hold it and answer when
1809
1926
  // the render finishes (onRunEnd flushes the queue + warms the model).
1810
- if (pauseLocalDuringGen &&
1811
- ollamaApi === "ollama" &&
1812
- backendForTab(event.tab_id) === "ollama" &&
1813
- QueueMonitor.isBusy()) {
1927
+ const tabIsLocalVram = (ollamaApi === "ollama" && backendForTab(event.tab_id) === "ollama") ||
1928
+ (backendForTab(event.tab_id) === "lmstudio" && isLocalLmstudio(LMSTUDIO_BASE_URL));
1929
+ if (pauseLocalDuringGen && tabIsLocalVram && QueueMonitor.isBusy()) {
1814
1930
  const key = agentKeyFor(event.tab_id);
1815
1931
  const arr = heldDuringGen.get(key) ?? [];
1816
1932
  arr.push({ text: outText, opts: sendOpts });