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.
- package/dist/orchestrator/backend-readiness.js +34 -0
- package/dist/orchestrator/backend-readiness.js.map +1 -1
- package/dist/orchestrator/index.js +132 -16
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/ollama-backend.js +9 -0
- package/dist/orchestrator/ollama-backend.js.map +1 -1
- package/dist/services/lmstudio-lifecycle.js +143 -0
- package/dist/services/lmstudio-lifecycle.js.map +1 -0
- package/dist/services/panel-settings.js +3 -0
- package/dist/services/panel-settings.js.map +1 -1
- package/package.json +5 -3
- package/scripts/gen-changelog.mjs +244 -0
|
@@ -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;
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
1461
|
-
? (
|
|
1462
|
-
:
|
|
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
|
-
:
|
|
1472
|
-
? `🟢 comfyui-mcp agent ready — ${agentLabel} via
|
|
1473
|
-
:
|
|
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
|
-
:
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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 });
|