comfyui-mcp 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/backend-readiness.js +80 -0
- package/dist/orchestrator/backend-readiness.js.map +1 -0
- package/dist/orchestrator/codex-backend.js +32 -4
- package/dist/orchestrator/codex-backend.js.map +1 -1
- package/dist/orchestrator/index.js +333 -154
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/panel-agent.js +49 -14
- package/dist/orchestrator/panel-agent.js.map +1 -1
- package/dist/services/env-capabilities.js +95 -1
- package/dist/services/env-capabilities.js.map +1 -1
- package/dist/services/image-management.js +100 -1
- package/dist/services/image-management.js.map +1 -1
- package/dist/services/manifest.js +71 -14
- package/dist/services/manifest.js.map +1 -1
- package/dist/services/model-resolver.js +143 -1
- package/dist/services/model-resolver.js.map +1 -1
- package/dist/services/node-management.js +46 -0
- package/dist/services/node-management.js.map +1 -1
- package/dist/tools/image-management.js +8 -4
- package/dist/tools/image-management.js.map +1 -1
- package/dist/tools/manifest.js +1 -1
- package/dist/tools/manifest.js.map +1 -1
- package/dist/tools/model-extras.js +27 -8
- package/dist/tools/model-extras.js.map +1 -1
- package/dist/tools/model-management.js +1 -1
- package/dist/tools/model-management.js.map +1 -1
- package/dist/tools/node-management.js +21 -0
- package/dist/tools/node-management.js.map +1 -1
- package/dist/transport/cli.js +39 -2
- package/dist/transport/cli.js.map +1 -1
- package/package.json +2 -2
- package/packs/wan-multitalk/install-runpod.sh +44 -0
- package/packs/wan-multitalk/install-windows.bat +49 -0
- package/packs/wan-multitalk/manifest.yaml +50 -0
- package/packs/wan-multitalk/pack.yaml +28 -0
- package/packs/wan-multitalk/workflow.json +1432 -0
- package/plugin/skills/triton-sageattention/SKILL.md +15 -0
- package/plugin/skills/troubleshooting/SKILL.md +56 -0
- package/plugin/skills/wan-multitalk/SKILL.md +97 -0
|
@@ -22,6 +22,7 @@ import { readUserMcpServers } from "../services/user-mcp-config.js";
|
|
|
22
22
|
import { buildComfyuiMcpEnv, comfyuiSecretKeys, onComfyuiSecretsChanged, } from "../services/panel-secrets.js";
|
|
23
23
|
import { CodexBackend } from "./codex-backend.js";
|
|
24
24
|
import { GeminiBackend, GEMINI_DEFAULT_MODEL } from "./gemini-backend.js";
|
|
25
|
+
import { allBackendReadiness } from "./backend-readiness.js";
|
|
25
26
|
import { startPanelMcpHttpServer } from "./panel-mcp-http.js";
|
|
26
27
|
import { readComfyuiCrashLog, formatCrashNote } from "../services/crash-log.js";
|
|
27
28
|
import { QueueMonitor } from "../services/queue-monitor.js";
|
|
@@ -56,6 +57,8 @@ RESOLVING A TANGLED / TOGGLE-HEAVY WORKFLOW (Get/Set buses + rgthree-bypassed pi
|
|
|
56
57
|
|
|
57
58
|
DOWNLOADING MODELS — use the download_model tool, NOT a raw shell download. When a workflow needs model weights you don't have (checkpoints, LoRAs, VAEs, text encoders, etc.), download them with the comfyui MCP download_model tool (or download_civitai_model for CivitAI): it streams the file into the correct ComfyUI models/ subfolder AND surfaces live progress in the panel's download tray so the user can watch it. Pass target_subfolder to land the file exactly where it belongs (e.g. 'loras', 'checkpoints', 'vae', 'text_encoders', or a nested path like 'loras/<subdir>'). Do NOT shell out to curl/wget/aria2 for model files — a raw shell download has no progress in the panel and can drop the file in the wrong place. Reserve the shell for things download_model can't do.
|
|
58
59
|
|
|
60
|
+
HARDWARE & RUNTIME STATS — use the MCP tools, NOT the shell. For GPU / VRAM / CPU / RAM, CUDA/torch/python versions, and ComfyUI runtime stats, call the comfyui MCP get_system_stats (raw /system_stats) or get_environment (a summarized snapshot) — they read the CONNECTED ComfyUI's /system_stats and work for LOCAL and REMOTE targets alike. Do NOT shell out (nvidia-smi, PowerShell, wmic, python) for hardware info: the managed shell is sandboxed/read-only and rejects multi-line scripts, so those probes fail and only reach the orchestrator host anyway, not a remote ComfyUI. The startup ENVIRONMENT line already summarizes the machine; when you need current or more detail, get_system_stats / get_environment are the source of truth.
|
|
61
|
+
|
|
59
62
|
LOCAL-GPU (FREE) vs API NODES (PAID CREDITS) — and ASK before spending. ComfyUI workflows are either LOCAL — they run on the user's OWN GPU, which is free — or they use API NODES (hosted/partner services) that consume the user's PAID api credits. The bundled installer packs (list_packs) are ALL local/free; ComfyUI's official templates and any ad-hoc or generated workflow MAY use API nodes. BEFORE you build OR load any workflow that uses API nodes, you MUST ASK the user whether to use the free local GPU or paid api credits, and NEVER silently spend credits. To tell the difference reliably, call check_workflow_runtime(pack:<name> or graph:<json>) — it returns { runtime: 'local'|'api'|'mixed'|'unknown', usesApiNodes, apiNodes[] } by scanning the graph's nodes against the server's API-node set; treat 'api'/'mixed' (usesApiNodes:true) AND 'unknown' (unclassifiable nodes that could be paid) as POSSIBLY PAID and stop to ask — only 'local' is confirmed free. DEFAULT TO / PREFER the local pack unless the user explicitly opts into API nodes. Packs are always safe to load without asking; check ad-hoc/template workflows first.
|
|
60
63
|
|
|
61
64
|
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.
|
|
@@ -342,11 +345,28 @@ export async function runPanelOrchestrator() {
|
|
|
342
345
|
// mode — so it generates against the live ComfyUI over COMFYUI_URL and never
|
|
343
346
|
// tries to bind the bridge port we own here.
|
|
344
347
|
const mcpEntry = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
348
|
+
// Mutable: the panel sends the ComfyUI URL it was SERVED FROM (window.location)
|
|
349
|
+
// in `hello`, and the orchestrator retargets to it (applyComfyuiUrl) — so
|
|
350
|
+
// `--panel-orchestrator` boots on the localhost default and auto-points at
|
|
351
|
+
// whatever ComfyUI (local or a RunPod proxy) the browser is actually on. No
|
|
352
|
+
// `connect <url>` needed.
|
|
353
|
+
let comfyuiUrl = process.env.COMFYUI_URL ?? "http://127.0.0.1:8188";
|
|
354
|
+
// ComfyUI install path — when set AND the target is loopback, the spawned agent's
|
|
355
|
+
// MCP runs in LOCAL mode (download_model / apply_manifest / installer-pack /
|
|
356
|
+
// model-scan tools). A REMOTE target (non-loopback) forces remote-only, so we
|
|
357
|
+
// drop the path. `envComfyuiPath` is the orchestrator's own env value; the live
|
|
358
|
+
// `comfyuiPath` is derived from it + the current target.
|
|
359
|
+
const envComfyuiPath = process.env.COMFYUI_PATH;
|
|
360
|
+
const isLoopbackUrl = (u) => {
|
|
361
|
+
try {
|
|
362
|
+
const h = new URL(u).hostname.toLowerCase();
|
|
363
|
+
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "0.0.0.0" || h === "";
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
let comfyuiPath = isLoopbackUrl(comfyuiUrl) ? envComfyuiPath : undefined;
|
|
350
370
|
const model = process.env.COMFYUI_MCP_PANEL_MODEL ?? "claude-opus-4-8";
|
|
351
371
|
const envEffort = process.env.COMFYUI_MCP_PANEL_EFFORT;
|
|
352
372
|
const effort = isEffort(envEffort) ? envEffort : undefined;
|
|
@@ -409,12 +429,31 @@ export async function runPanelOrchestrator() {
|
|
|
409
429
|
// COMFYUI_MCP_GEMINI_MODEL (default gemini-2.5-pro). The model is applied at spawn
|
|
410
430
|
// via the CLI `--model` flag (ACP exposes no per-session model setter).
|
|
411
431
|
const geminiModel = process.env.COMFYUI_MCP_GEMINI_MODEL ?? GEMINI_DEFAULT_MODEL;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
432
|
+
// ── Per-tab backend (single-port multi-provider) ──────────────────────────
|
|
433
|
+
// ONE orchestrator on ONE bridge port serves ALL providers; the panel picks a
|
|
434
|
+
// provider per tab via the `hello`/`set_backend` handshake, instead of the node
|
|
435
|
+
// spawning one process per provider on its own port (9180/9181/9182). Internally
|
|
436
|
+
// each (panel tab, backend) pair is one agent addressed by a composite key
|
|
437
|
+
// `tabId::backend`, so switching provider starts a FRESH session for that
|
|
438
|
+
// provider (the panel replays the transcript to seed it) while a same-provider
|
|
439
|
+
// reconnect RESUMES. `backendId`/`codexModel`/`geminiModel` above are the
|
|
440
|
+
// DEFAULT + per-provider model config; the process is no longer pinned to one.
|
|
441
|
+
const KNOWN_BACKENDS = new Set(["claude", "codex", "gemini"]);
|
|
442
|
+
const defaultBackend = KNOWN_BACKENDS.has(backendId) ? backendId : "claude";
|
|
443
|
+
const AGENT_KEY_SEP = "::";
|
|
444
|
+
const tabBackends = new Map(); // panel tabId -> selected backend
|
|
445
|
+
const backendForTab = (panelTabId) => tabBackends.get(panelTabId) ?? defaultBackend;
|
|
446
|
+
const agentKeyFor = (panelTabId) => panelTabId + AGENT_KEY_SEP + backendForTab(panelTabId);
|
|
447
|
+
// A panel tab id never contains "::"; backend names never do — so split on the
|
|
448
|
+
// LAST separator to recover each half from a composite key.
|
|
449
|
+
const panelTabOf = (key) => {
|
|
450
|
+
const i = key.lastIndexOf(AGENT_KEY_SEP);
|
|
451
|
+
return i >= 0 ? key.slice(0, i) : key;
|
|
452
|
+
};
|
|
453
|
+
const backendOf = (key) => {
|
|
454
|
+
const i = key.lastIndexOf(AGENT_KEY_SEP);
|
|
455
|
+
return i >= 0 ? key.slice(i + AGENT_KEY_SEP.length) : defaultBackend;
|
|
456
|
+
};
|
|
418
457
|
// ---- live ENVIRONMENT-CAPABILITIES block ----
|
|
419
458
|
// Gather the machine's facts ONCE at startup (CACHED) — OS/CPU/RAM from node,
|
|
420
459
|
// GPU/VRAM/CUDA/torch/python/ComfyUI from /system_stats, Triton/SageAttention by
|
|
@@ -462,7 +501,9 @@ export async function runPanelOrchestrator() {
|
|
|
462
501
|
// A panel-saved tool secret (CIVITAI_API_TOKEN, HF_TOKEN, …) is layered on top
|
|
463
502
|
// by buildComfyuiMcpEnv() at SPAWN time, so the same headless tool surface — and
|
|
464
503
|
// the same secrets — reach either provider.
|
|
465
|
-
|
|
504
|
+
// A FUNCTION (not a frozen object) so it always reflects the CURRENT retargeted
|
|
505
|
+
// comfyuiUrl/comfyuiPath — makeHttpBackendMcpServers calls it per (re)spawn.
|
|
506
|
+
const comfyuiBaseEnv = () => ({
|
|
466
507
|
COMFYUI_URL: comfyuiUrl,
|
|
467
508
|
COMFYUI_MCP_PROGRESS_DIR: progressDir,
|
|
468
509
|
...(comfyuiPath ? { COMFYUI_PATH: comfyuiPath } : {}),
|
|
@@ -472,19 +513,23 @@ export async function runPanelOrchestrator() {
|
|
|
472
513
|
...(process.env.HF_TOKEN ? { HF_TOKEN: process.env.HF_TOKEN } : {}),
|
|
473
514
|
// Test-only tool-call trace (knowledge-parity smoke). No-op unless set.
|
|
474
515
|
...(process.env.COMFYUI_MCP_TOOL_TRACE ? { COMFYUI_MCP_TOOL_TRACE: process.env.COMFYUI_MCP_TOOL_TRACE } : {}),
|
|
475
|
-
};
|
|
516
|
+
});
|
|
476
517
|
// The orchestrator-hosted loopback HTTP MCP for panel_* tools. Started for the
|
|
477
518
|
// non-Claude backends (Codex + Gemini), which can't host an in-process SDK MCP
|
|
478
519
|
// server the way Claude does. Port: COMFYUI_MCP_PANEL_MCP_PORT, default
|
|
479
520
|
// bridgePort+1 (loopback only).
|
|
521
|
+
// Start the loopback HTTP panel-MCP ALWAYS: with single-port multi-provider any
|
|
522
|
+
// tab may pick codex/gemini at runtime, and those backends drive the canvas
|
|
523
|
+
// through this server (Claude tabs use the in-process SDK server instead). The
|
|
524
|
+
// per-tab session routing (`urlFor(panelTabId)`) already isolates tabs.
|
|
480
525
|
let panelMcpHttp = null;
|
|
481
|
-
|
|
526
|
+
{
|
|
482
527
|
const panelMcpPort = Number(process.env.COMFYUI_MCP_PANEL_MCP_PORT) || bridgePort + 1;
|
|
483
528
|
try {
|
|
484
529
|
panelMcpHttp = await startPanelMcpHttpServer(bridge, panelMcpPort);
|
|
485
530
|
}
|
|
486
531
|
catch (err) {
|
|
487
|
-
logger.error(`[panel-orchestrator] could not start the panel HTTP MCP on :${panelMcpPort} —
|
|
532
|
+
logger.error(`[panel-orchestrator] could not start the panel HTTP MCP on :${panelMcpPort} — codex/gemini tabs will lack live-graph tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
488
533
|
}
|
|
489
534
|
}
|
|
490
535
|
// Shared MCP server config for BOTH the Codex and Gemini backends — they take an
|
|
@@ -498,52 +543,61 @@ export async function runPanelOrchestrator() {
|
|
|
498
543
|
args: [mcpEntry], // dist/index.js
|
|
499
544
|
// Merge persisted tool secrets at SPAWN time so a respawn picks up a
|
|
500
545
|
// just-saved CIVITAI_API_TOKEN / HF_TOKEN without a process restart.
|
|
501
|
-
env: buildComfyuiMcpEnv(comfyuiBaseEnv),
|
|
546
|
+
env: buildComfyuiMcpEnv(comfyuiBaseEnv()),
|
|
502
547
|
},
|
|
503
548
|
// Live-graph panel_* tools for THIS tab over the loopback HTTP MCP.
|
|
504
549
|
...(panelMcpHttp
|
|
505
550
|
? { panel: { transport: "http", url: panelMcpHttp.urlFor(tabId) } }
|
|
506
551
|
: {}),
|
|
507
552
|
});
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
553
|
+
// Build the provider backend for a composite agent key `panelTabId::backend`.
|
|
554
|
+
// Claude → undefined (PanelAgent uses its built-in in-process SDK backend);
|
|
555
|
+
// codex/gemini → their CLI-driven backend, wired to the panel_* tools over the
|
|
556
|
+
// loopback HTTP MCP for THIS panel tab's canvas (comfyuiUrl gives vision parity).
|
|
557
|
+
const makeBackend = (key) => {
|
|
558
|
+
const backend = backendOf(key);
|
|
559
|
+
const panelTabId = panelTabOf(key);
|
|
560
|
+
if (backend === "codex") {
|
|
561
|
+
return new CodexBackend({
|
|
562
|
+
cwd: comfyuiPath ?? process.cwd(),
|
|
563
|
+
model: codexModel,
|
|
564
|
+
systemAppend: panelSystemAppend,
|
|
565
|
+
comfyuiUrl,
|
|
566
|
+
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (backend === "gemini") {
|
|
570
|
+
return new GeminiBackend({
|
|
520
571
|
cwd: comfyuiPath ?? process.cwd(),
|
|
521
572
|
model: geminiModel,
|
|
522
573
|
systemAppend: panelSystemAppend,
|
|
523
|
-
// Base ComfyUI URL so the backend can fetch image bytes from /view and
|
|
524
|
-
// deliver them inline as base64 image ContentBlocks (vision parity).
|
|
525
574
|
comfyuiUrl,
|
|
526
|
-
mcpServers: makeHttpBackendMcpServers(
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
575
|
+
mcpServers: makeHttpBackendMcpServers(panelTabId),
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return undefined; // claude → built-in ClaudeBackend
|
|
579
|
+
};
|
|
580
|
+
logger.info(`[panel-orchestrator] single-port multi-provider: default backend=${defaultBackend}; ` +
|
|
581
|
+
`codex/gemini panel_* live-graph tools via loopback HTTP MCP${panelMcpHttp ? ` on :${panelMcpHttp.port}` : " UNAVAILABLE"} + headless comfyui MCP`);
|
|
582
|
+
// Readiness/model probing routes through the SELECTED backend PER TAB — a
|
|
583
|
+
// codex/gemini tab's "ready" must NOT depend on Claude SDK/login health. Claude
|
|
584
|
+
// uses fetchSupportedModels(); codex/gemini spin up a throwaway probe backend
|
|
585
|
+
// (which also proves the CLI can launch). Cached per backend so repeated hellos
|
|
586
|
+
// don't re-probe.
|
|
587
|
+
const probeBackends = new Map();
|
|
588
|
+
const getProbeBackend = (backend) => {
|
|
589
|
+
if (backend === "claude")
|
|
590
|
+
return null; // claude uses the SDK probe below
|
|
591
|
+
let pb = probeBackends.get(backend);
|
|
592
|
+
if (!pb) {
|
|
593
|
+
pb =
|
|
594
|
+
backend === "codex"
|
|
595
|
+
? new CodexBackend({ cwd: comfyuiPath ?? process.cwd(), model: codexModel })
|
|
596
|
+
: new GeminiBackend({ cwd: comfyuiPath ?? process.cwd(), model: geminiModel });
|
|
597
|
+
probeBackends.set(backend, pb);
|
|
598
|
+
}
|
|
599
|
+
return pb;
|
|
600
|
+
};
|
|
547
601
|
// Durable per-tab session ids (keyed by our bridge port), so a tab's agent
|
|
548
602
|
// resumes its conversation even after the orchestrator PROCESS is killed and
|
|
549
603
|
// respawned (a wedge auto-restart) — not just a soft reload.
|
|
@@ -577,42 +631,46 @@ export async function runPanelOrchestrator() {
|
|
|
577
631
|
comfyuiUrl, // for fetching image bytes to inline into agent turns
|
|
578
632
|
systemAppend: panelSystemAppend,
|
|
579
633
|
pluginPath: pluginAvailable ? pluginPath : undefined,
|
|
580
|
-
//
|
|
581
|
-
|
|
634
|
+
// In-process live-graph MCP for CLAUDE keys only (codex/gemini drive the
|
|
635
|
+
// canvas through the loopback HTTP MCP instead). Bound to the PANEL tab so
|
|
636
|
+
// panel_* tools reach the user's canvas regardless of the composite key.
|
|
637
|
+
makePanelServer: (key) => backendOf(key) === "claude" ? createPanelMcpServer(bridge, panelTabOf(key)) : undefined,
|
|
582
638
|
mcpServers: buildMcpServers(),
|
|
583
|
-
|
|
639
|
+
// NOTE: manager callbacks fire with the composite agent key `tabId::backend`;
|
|
640
|
+
// panelTabOf() recovers the PANEL tab so every push reaches the right socket.
|
|
641
|
+
onSay: (key, text, meta) => {
|
|
584
642
|
// `id` lets the panel reconcile this committed message with its live
|
|
585
643
|
// streaming preview (same id) instead of rendering a duplicate bubble.
|
|
586
|
-
bridge.push({ type: "say", text, id: meta?.id, streamed: meta?.streamed },
|
|
644
|
+
bridge.push({ type: "say", text, id: meta?.id, streamed: meta?.streamed }, panelTabOf(key));
|
|
587
645
|
},
|
|
588
646
|
// Live streaming deltas → the panel's think-window + streaming reply bubble.
|
|
589
|
-
onStream: (
|
|
590
|
-
bridge.push({ type: "stream", phase: ev.phase, id: ev.id, delta: ev.delta },
|
|
647
|
+
onStream: (key, ev) => {
|
|
648
|
+
bridge.push({ type: "stream", phase: ev.phase, id: ev.id, delta: ev.delta }, panelTabOf(key));
|
|
591
649
|
},
|
|
592
650
|
// Per-response usage → the panel's context/usage meter (updates live).
|
|
593
|
-
onStatus: pushStatus,
|
|
651
|
+
onStatus: (key, status) => pushStatus(panelTabOf(key), status),
|
|
594
652
|
// Report the SDK session id so the panel can persist it and resume on reload.
|
|
595
|
-
onSession: (
|
|
596
|
-
bridge.push({ type: "session", session_id: sessionId },
|
|
653
|
+
onSession: (key, sessionId) => {
|
|
654
|
+
bridge.push({ type: "session", session_id: sessionId }, panelTabOf(key));
|
|
597
655
|
},
|
|
598
656
|
// Per-turn rewind anchor (assistant UUID) → the panel stores it so a later
|
|
599
657
|
// "rewind conversation to here" can fork the session at that point.
|
|
600
|
-
onTurnAnchor: (
|
|
601
|
-
bridge.push({ type: "turn_anchor", uuid },
|
|
658
|
+
onTurnAnchor: (key, uuid) => {
|
|
659
|
+
bridge.push({ type: "turn_anchor", uuid }, panelTabOf(key));
|
|
602
660
|
},
|
|
603
661
|
// Turn lifecycle → the panel's "working" indicator (stays up through silent
|
|
604
662
|
// tool work; clears on done).
|
|
605
|
-
onTurn: (
|
|
606
|
-
bridge.push({ type: "turn", state },
|
|
663
|
+
onTurn: (key, state) => {
|
|
664
|
+
bridge.push({ type: "turn", state }, panelTabOf(key));
|
|
607
665
|
},
|
|
608
666
|
// Live extended-thinking token count → "thinking… (N)" indicator.
|
|
609
|
-
onThinking: (
|
|
610
|
-
bridge.push({ type: "thinking", tokens },
|
|
667
|
+
onThinking: (key, tokens) => {
|
|
668
|
+
bridge.push({ type: "thinking", tokens }, panelTabOf(key));
|
|
611
669
|
},
|
|
612
670
|
// The agent dequeued a message (the true "read" moment) → flip that bubble
|
|
613
671
|
// from queued/muted to read.
|
|
614
|
-
onSeen: (
|
|
615
|
-
bridge.push({ type: "ack", ok: true, kind: "seen", mid },
|
|
672
|
+
onSeen: (key, mid) => {
|
|
673
|
+
bridge.push({ type: "ack", ok: true, kind: "seen", mid }, panelTabOf(key));
|
|
616
674
|
},
|
|
617
675
|
// ROOT-CAUSE self-exit (the "bridge open but no panel agent responded" wedge):
|
|
618
676
|
// a tab's agent died fatally (couldn't start, or its bounded self-restart gave
|
|
@@ -629,6 +687,52 @@ export async function runPanelOrchestrator() {
|
|
|
629
687
|
// Let refreshEnvCapabilities() feed a freshly-gathered env block into agents
|
|
630
688
|
// spawned after a ComfyUI restart/reconnect.
|
|
631
689
|
liveManager = manager;
|
|
690
|
+
// Retarget the live ComfyUI from the panel's `hello.comfyui_url` (the URL the
|
|
691
|
+
// browser was SERVED FROM — window.location). This is what lets a bare
|
|
692
|
+
// `--panel-orchestrator` (booted on the localhost default) auto-point at whatever
|
|
693
|
+
// ComfyUI the user actually has open — local OR a RunPod proxy — with no
|
|
694
|
+
// `connect <url>`. Loopback → LOCAL mode (keep COMFYUI_PATH); non-loopback →
|
|
695
|
+
// REMOTE mode (drop the path). No-op if unchanged. Returns true if it retargeted.
|
|
696
|
+
const applyComfyuiUrl = (rawUrl) => {
|
|
697
|
+
if (typeof rawUrl !== "string")
|
|
698
|
+
return false;
|
|
699
|
+
const next = rawUrl.trim().replace(/\/+$/, "");
|
|
700
|
+
if (!next)
|
|
701
|
+
return false;
|
|
702
|
+
let host;
|
|
703
|
+
try {
|
|
704
|
+
const parsed = new URL(next);
|
|
705
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
706
|
+
return false;
|
|
707
|
+
host = parsed.hostname;
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return false; // not a valid URL — ignore (keep current target)
|
|
711
|
+
}
|
|
712
|
+
if (!host || next === comfyuiUrl)
|
|
713
|
+
return false;
|
|
714
|
+
const prev = comfyuiUrl;
|
|
715
|
+
comfyuiUrl = next;
|
|
716
|
+
comfyuiPath = isLoopbackUrl(next) ? envComfyuiPath : undefined;
|
|
717
|
+
// Point every provider at the new target: Claude via its rebuilt MCP env, the
|
|
718
|
+
// manager's image-fetch URL, then respawn active agents so the live comfyui MCP
|
|
719
|
+
// subprocess is recreated with the new COMFYUI_URL (no-op if none are running —
|
|
720
|
+
// the next spawn picks it up from the now-updated closures).
|
|
721
|
+
manager.setMcpServers(buildMcpServers());
|
|
722
|
+
manager.setComfyuiUrl(comfyuiUrl);
|
|
723
|
+
manager.restartAllForMcpEnv();
|
|
724
|
+
// Re-point the render watchdog and re-probe the env (remote vs local differs).
|
|
725
|
+
try {
|
|
726
|
+
QueueMonitor.stop();
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
/* best-effort */
|
|
730
|
+
}
|
|
731
|
+
QueueMonitor.start(comfyuiUrl);
|
|
732
|
+
void refreshEnvCapabilities();
|
|
733
|
+
logger.info(`[panel-orchestrator] retargeted ComfyUI ${prev} → ${comfyuiUrl} (${isLoopbackUrl(next) ? "local" : "remote"} mode) from panel hello`);
|
|
734
|
+
return true;
|
|
735
|
+
};
|
|
632
736
|
// Tool secrets → comfyui MCP env: when the user saves a token via
|
|
633
737
|
// panel_request_secret (e.g. CIVITAI_API_TOKEN for download_civitai_model), the
|
|
634
738
|
// secret store persists it and fires this. We rebuild the comfyui server's spawn
|
|
@@ -650,52 +754,59 @@ export async function runPanelOrchestrator() {
|
|
|
650
754
|
// works on the subscription lane) and cached. Pushed to each tab so the
|
|
651
755
|
// panel's model/effort picker reflects what's actually available, with each
|
|
652
756
|
// model's supported effort levels, instead of a hardcoded list.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const probe =
|
|
666
|
-
? Promise.resolve(
|
|
667
|
-
.then(() =>
|
|
668
|
-
.then((list) =>
|
|
669
|
-
// Carry the effort metadata through to the panel's ModelInfo — without
|
|
670
|
-
// this the supportsEffort/supportedEffortLevels the Codex backend
|
|
671
|
-
// advertises get dropped here and the panel hides the effort dropdown.
|
|
672
|
-
list.map((m) => ({
|
|
757
|
+
// Model list PER BACKEND — probed lazily and cached; an empty/failed probe is
|
|
758
|
+
// NOT cached so the next hello retries. Claude uses fetchSupportedModels() (the
|
|
759
|
+
// only path that works on the subscription lane); codex/gemini enumerate via a
|
|
760
|
+
// throwaway probe backend, which also proves the CLI can launch (= readiness).
|
|
761
|
+
// (Gemini's probe proves the CLI + ACP handshake but not Google sign-in, which
|
|
762
|
+
// ACP only reports at session/new — so its "ready" is provisional; a signed-out
|
|
763
|
+
// CLI surfaces a clear one-shot error on the first turn.)
|
|
764
|
+
const modelsByBackend = new Map();
|
|
765
|
+
function ensureModels(backend) {
|
|
766
|
+
let p = modelsByBackend.get(backend);
|
|
767
|
+
if (!p) {
|
|
768
|
+
const pb = getProbeBackend(backend);
|
|
769
|
+
const probe = pb
|
|
770
|
+
? Promise.resolve(pb.prepare?.())
|
|
771
|
+
.then(() => pb.listModels())
|
|
772
|
+
.then((list) => list.map((m) => ({
|
|
673
773
|
value: m.id,
|
|
674
774
|
displayName: m.label ?? m.id,
|
|
675
775
|
...(m.supportsEffort != null ? { supportsEffort: m.supportsEffort } : {}),
|
|
676
776
|
...(m.supportedEffortLevels ? { supportedEffortLevels: m.supportedEffortLevels } : {}),
|
|
677
777
|
})))
|
|
678
778
|
.catch((err) => {
|
|
679
|
-
logger.warn(`[panel-orchestrator]
|
|
779
|
+
logger.warn(`[panel-orchestrator] ${backend} model probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
680
780
|
return [];
|
|
681
781
|
})
|
|
682
782
|
: fetchSupportedModels(model);
|
|
683
|
-
|
|
684
|
-
// Don't cache an empty/failed probe forever — let the next hello retry.
|
|
783
|
+
p = probe.then((list) => {
|
|
685
784
|
if (!list.length)
|
|
686
|
-
|
|
785
|
+
modelsByBackend.delete(backend); // don't cache a failure
|
|
687
786
|
return list;
|
|
688
787
|
});
|
|
788
|
+
modelsByBackend.set(backend, p);
|
|
689
789
|
}
|
|
690
|
-
return
|
|
790
|
+
return p;
|
|
791
|
+
}
|
|
792
|
+
// The model to highlight as "current" for a backend: the panel's configured
|
|
793
|
+
// model for claude; the env override (or account default = the list's own
|
|
794
|
+
// current) for codex/gemini.
|
|
795
|
+
function currentModelFor(backend) {
|
|
796
|
+
if (backend === "codex")
|
|
797
|
+
return codexModel;
|
|
798
|
+
if (backend === "gemini")
|
|
799
|
+
return geminiModel;
|
|
800
|
+
return model;
|
|
691
801
|
}
|
|
692
|
-
function pushModels(
|
|
693
|
-
|
|
802
|
+
function pushModels(panelTabId) {
|
|
803
|
+
const backend = backendForTab(panelTabId);
|
|
804
|
+
void ensureModels(backend)
|
|
694
805
|
.then((models) => {
|
|
695
806
|
if (models.length) {
|
|
696
|
-
// `backend` rides on the models frame so the panel's
|
|
697
|
-
//
|
|
698
|
-
bridge.push({ type: "models", models, current:
|
|
807
|
+
// `backend` rides on the models frame so the panel's picker reflects the
|
|
808
|
+
// provider THIS tab selected (single-port multi-provider).
|
|
809
|
+
bridge.push({ type: "models", models, current: currentModelFor(backend), backend }, panelTabId);
|
|
699
810
|
}
|
|
700
811
|
})
|
|
701
812
|
.catch(() => {
|
|
@@ -720,10 +831,8 @@ export async function runPanelOrchestrator() {
|
|
|
720
831
|
// the built-ins that make sense inside the ComfyUI panel chat.
|
|
721
832
|
const PANEL_SLASH_ALLOWLIST = new Set(["compact", "context", "usage", "loop", "goal", "clear"]);
|
|
722
833
|
function pushCommands(tabId) {
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
if (isHttpPanelBackend)
|
|
726
|
-
return;
|
|
834
|
+
// Claude-only: SDK slash-commands don't exist for codex/gemini. Callers already
|
|
835
|
+
// gate this to claude tabs (single-port multi-provider), so no backend check here.
|
|
727
836
|
void ensureCommands()
|
|
728
837
|
.then((commands) => {
|
|
729
838
|
const useful = commands.filter((c) => PANEL_SLASH_ALLOWLIST.has(c.name));
|
|
@@ -734,75 +843,132 @@ export async function runPanelOrchestrator() {
|
|
|
734
843
|
/* probe already logged; panel just won't show SDK commands */
|
|
735
844
|
});
|
|
736
845
|
}
|
|
846
|
+
// Real per-provider readiness, computed HERE (the machine running the agents)
|
|
847
|
+
// and pushed over the bridge so the panel's provider switcher reflects the
|
|
848
|
+
// truth — not the ComfyUI host's probe, which is blind to the laptop in the
|
|
849
|
+
// "remote ComfyUI, local agent" model (and never sees Claude's SDK, which has
|
|
850
|
+
// no CLI). The panel prefers this frame over its GET /backends probe.
|
|
851
|
+
function pushReadiness(tabId) {
|
|
852
|
+
try {
|
|
853
|
+
const { backends, any_ready } = allBackendReadiness(KNOWN_BACKENDS);
|
|
854
|
+
bridge.push({ type: "backends", backends, any_ready }, tabId);
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
logger.warn(`[panel-orchestrator] readiness probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
737
860
|
bridge.onPanelMessage = (event) => {
|
|
738
861
|
// Connect ack: the instant a panel tab connects, the orchestrator announces
|
|
739
862
|
// itself so "connected" means "a real agent is attending" — not merely "a
|
|
740
863
|
// socket is open." A bare/undriven bridge stays silent, so the panel can
|
|
741
864
|
// tell the difference (and warn if no ack arrives).
|
|
742
865
|
if (event.type === "hello" && event.tab_id) {
|
|
743
|
-
|
|
744
|
-
//
|
|
866
|
+
const panelTab = event.tab_id;
|
|
867
|
+
// Retarget ComfyUI to the URL the browser was served from (window.location),
|
|
868
|
+
// BEFORE the readiness probe so the "ready" ack reflects the right instance.
|
|
869
|
+
applyComfyuiUrl(event.comfyui_url);
|
|
870
|
+
// Per-tab backend selection (single-port multi-provider). The panel names
|
|
871
|
+
// its chosen provider on connect (and on a switch it re-sends hello / a
|
|
872
|
+
// set_backend); absent or unknown → the default.
|
|
873
|
+
const reqBackend = typeof event.backend === "string"
|
|
874
|
+
? event.backend.toLowerCase()
|
|
875
|
+
: undefined;
|
|
876
|
+
const backend = reqBackend && KNOWN_BACKENDS.has(reqBackend) ? reqBackend : defaultBackend;
|
|
877
|
+
const prev = tabBackends.get(panelTab);
|
|
878
|
+
if (prev && prev !== backend) {
|
|
879
|
+
// Provider switch: retire the previous provider's agent for this tab so it
|
|
880
|
+
// doesn't linger. The new provider starts a FRESH session (the panel
|
|
881
|
+
// replays the transcript as context on its first message to seed it).
|
|
882
|
+
manager.reset(panelTab + AGENT_KEY_SEP + prev);
|
|
883
|
+
}
|
|
884
|
+
tabBackends.set(panelTab, backend);
|
|
885
|
+
const key = panelTab + AGENT_KEY_SEP + backend;
|
|
886
|
+
// Reload restore: the panel re-sends the last session id it saw. Honored
|
|
887
|
+
// only for a SAME-provider (re)connect — a switch always starts fresh. The
|
|
888
|
+
// orchestrator's own store stays authoritative on the actual spawn.
|
|
745
889
|
const resume = typeof event.resume === "string" ? event.resume : undefined;
|
|
746
|
-
if (resume)
|
|
747
|
-
manager.setResume(
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
890
|
+
if (resume && (!prev || prev === backend))
|
|
891
|
+
manager.setResume(key, resume);
|
|
892
|
+
// Live model list for the picker; SDK slash commands are Claude-only.
|
|
893
|
+
pushModels(panelTab);
|
|
894
|
+
// Truthful provider readiness (this machine runs the agents), so the
|
|
895
|
+
// switcher stops falsely showing "CLI not installed" behind a remote pod.
|
|
896
|
+
pushReadiness(panelTab);
|
|
897
|
+
if (backend === "claude")
|
|
898
|
+
pushCommands(panelTab);
|
|
752
899
|
// Re-push the last usage so the context meter isn't blank after a reload.
|
|
753
|
-
const lastStatus = manager.lastStatusFor(
|
|
900
|
+
const lastStatus = manager.lastStatusFor(key);
|
|
754
901
|
if (lastStatus)
|
|
755
|
-
pushStatus(
|
|
756
|
-
const tabId = event.tab_id;
|
|
902
|
+
pushStatus(panelTab, lastStatus);
|
|
757
903
|
const now = Date.now();
|
|
758
|
-
if (now - (lastAckAt.get(
|
|
904
|
+
if (now - (lastAckAt.get(panelTab) ?? 0) < ACK_DEBOUNCE_MS)
|
|
759
905
|
return;
|
|
760
|
-
lastAckAt.set(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
|
|
906
|
+
lastAckAt.set(panelTab, now);
|
|
907
|
+
const isCx = backend === "codex";
|
|
908
|
+
const isGm = backend === "gemini";
|
|
909
|
+
// TRUTHFUL "connected": only claim ready after PROVING the SELECTED backend
|
|
910
|
+
// can run, by probing its model list. If the probe fails — the "connected
|
|
911
|
+
// but dead" wedge — send a degraded ack so the panel shows the real state.
|
|
912
|
+
void ensureModels(backend)
|
|
766
913
|
.then((models) => {
|
|
767
914
|
if (models.length) {
|
|
768
|
-
|
|
769
|
-
// a WS blip, or a real restart (all carry `resume`) — the user already
|
|
770
|
-
// has their thread, so re-greeting is just noise. The ack still fires.
|
|
771
|
-
// Backend-appropriate messaging (P1-2): each provider must name its own
|
|
772
|
-
// account/auth, and the agent label is that provider's model (Codex/
|
|
773
|
-
// Gemini account default when the env override is unset).
|
|
774
|
-
const agentLabel = isCodex
|
|
915
|
+
const agentLabel = isCx
|
|
775
916
|
? (codexModel ?? models[0].value ?? "Codex")
|
|
776
|
-
:
|
|
917
|
+
: isGm
|
|
777
918
|
? (geminiModel ?? models[0].value ?? "Gemini")
|
|
778
919
|
: model;
|
|
920
|
+
// Greet only on a FRESH session (a resume/reconnect already has the thread).
|
|
779
921
|
if (!resume) {
|
|
780
|
-
const readyText =
|
|
922
|
+
const readyText = isCx
|
|
781
923
|
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Codex (ChatGPT) account. Ask away.`
|
|
782
|
-
:
|
|
924
|
+
: isGm
|
|
783
925
|
? `🟢 comfyui-mcp agent ready — ${agentLabel} on your Google account (Gemini Code Assist). Ask away.`
|
|
784
926
|
: `🟢 comfyui-mcp agent ready — ${agentLabel} on your Claude subscription. Ask away.`;
|
|
785
|
-
bridge.push({ type: "say", text: readyText },
|
|
927
|
+
bridge.push({ type: "say", text: readyText }, panelTab);
|
|
786
928
|
}
|
|
787
|
-
bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend
|
|
788
|
-
logger.info(`[panel-orchestrator] tab ${
|
|
929
|
+
bridge.push({ type: "ack", ok: true, kind: "ready", agent: agentLabel, backend }, panelTab);
|
|
930
|
+
logger.info(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) — agent healthy, ready ack`);
|
|
789
931
|
}
|
|
790
932
|
else {
|
|
791
|
-
const degradedText =
|
|
933
|
+
const degradedText = isCx
|
|
792
934
|
? "⚠️ The background agent isn't responding — the Codex app-server couldn't start. Make sure Codex is installed and signed in (run `codex login`), then Disconnect → Connect to retry."
|
|
793
|
-
:
|
|
935
|
+
: isGm
|
|
794
936
|
? "⚠️ 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."
|
|
795
937
|
: "⚠️ 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.";
|
|
796
|
-
bridge.push({ type: "say", text: degradedText },
|
|
797
|
-
bridge.push({ type: "ack", ok: false, kind: "degraded" },
|
|
798
|
-
logger.warn(`[panel-orchestrator] tab ${
|
|
938
|
+
bridge.push({ type: "say", text: degradedText }, panelTab);
|
|
939
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
|
|
940
|
+
logger.warn(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} connected (${backend}) but model probe empty — degraded ack`);
|
|
799
941
|
}
|
|
800
942
|
})
|
|
801
943
|
.catch(() => {
|
|
802
|
-
bridge.push({ type: "ack", ok: false, kind: "degraded" },
|
|
944
|
+
bridge.push({ type: "ack", ok: false, kind: "degraded" }, panelTab);
|
|
803
945
|
});
|
|
804
946
|
return;
|
|
805
947
|
}
|
|
948
|
+
// Provider switch WITHOUT a reconnect (single-port multi-provider): the panel
|
|
949
|
+
// picked a different backend chip. Retire the old provider's agent, remember
|
|
950
|
+
// the new one, and re-advertise its models. The panel replays the transcript
|
|
951
|
+
// as context on its next message so the fresh provider has the conversation.
|
|
952
|
+
if (event.type === "set_backend" && event.tab_id) {
|
|
953
|
+
const panelTab = event.tab_id;
|
|
954
|
+
const reqBackend = typeof event.backend === "string"
|
|
955
|
+
? event.backend.toLowerCase()
|
|
956
|
+
: "";
|
|
957
|
+
if (!KNOWN_BACKENDS.has(reqBackend)) {
|
|
958
|
+
bridge.push({ type: "ack", ok: false, kind: "set_backend", message: `unknown backend '${reqBackend}'` }, panelTab);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const prev = tabBackends.get(panelTab) ?? defaultBackend;
|
|
962
|
+
if (prev !== reqBackend)
|
|
963
|
+
manager.reset(panelTab + AGENT_KEY_SEP + prev);
|
|
964
|
+
tabBackends.set(panelTab, reqBackend);
|
|
965
|
+
pushModels(panelTab);
|
|
966
|
+
if (reqBackend === "claude")
|
|
967
|
+
pushCommands(panelTab);
|
|
968
|
+
bridge.push({ type: "ack", ok: true, kind: "set_backend", backend: reqBackend }, panelTab);
|
|
969
|
+
logger.info(`[panel-orchestrator] tab ${panelTab.slice(0, 8)} switched backend ${prev} → ${reqBackend}`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
806
972
|
// Live panel config (currently just the render-stall threshold). Applied
|
|
807
973
|
// immediately, no reconnect — the next turn's watchdog check uses the new
|
|
808
974
|
// value. Sent by the panel on connect and whenever the setting changes.
|
|
@@ -830,13 +996,13 @@ export async function runPanelOrchestrator() {
|
|
|
830
996
|
// makes the SDK session hang on init. (Defense in depth; the panel only
|
|
831
997
|
// sends ids from the live catalog.)
|
|
832
998
|
if (nextModel) {
|
|
833
|
-
const known = await ensureModels().catch(() => []);
|
|
999
|
+
const known = await ensureModels(backendForTab(tabId)).catch(() => []);
|
|
834
1000
|
if (known.length && !known.some((m) => m.value === nextModel)) {
|
|
835
1001
|
logger.warn(`[panel-orchestrator] ignoring unknown model "${nextModel}" — keeping current`);
|
|
836
1002
|
nextModel = undefined;
|
|
837
1003
|
}
|
|
838
1004
|
}
|
|
839
|
-
const applied = await manager.setOptions(tabId, { model: nextModel, effort: nextEffort });
|
|
1005
|
+
const applied = await manager.setOptions(agentKeyFor(tabId), { model: nextModel, effort: nextEffort });
|
|
840
1006
|
bridge.push({
|
|
841
1007
|
type: "ack",
|
|
842
1008
|
ok: true,
|
|
@@ -862,11 +1028,11 @@ export async function runPanelOrchestrator() {
|
|
|
862
1028
|
// look at me") so the agent stops and fixes it instead of running blind.
|
|
863
1029
|
// Everything else (e.g. a finished render's images) is enqueued normally.
|
|
864
1030
|
if (ev.kind === "run_error") {
|
|
865
|
-
void manager.injectRunError(event.tab_id, ev.error ?? "unknown error");
|
|
1031
|
+
void manager.injectRunError(agentKeyFor(event.tab_id), ev.error ?? "unknown error");
|
|
866
1032
|
logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} run_error → agent (interrupt)`);
|
|
867
1033
|
return;
|
|
868
1034
|
}
|
|
869
|
-
const delivered = manager.injectEvent(event.tab_id, ev);
|
|
1035
|
+
const delivered = manager.injectEvent(agentKeyFor(event.tab_id), ev);
|
|
870
1036
|
if (delivered) {
|
|
871
1037
|
logger.info(`[panel-orchestrator] tab ${event.tab_id.slice(0, 8)} event → agent: ${event.kind}`);
|
|
872
1038
|
}
|
|
@@ -880,7 +1046,7 @@ export async function runPanelOrchestrator() {
|
|
|
880
1046
|
// interrupted turn so BOTH messages get answered; a plain Stop/Ctrl+C/Esc
|
|
881
1047
|
// sends a bare interrupt and must NOT re-run the stopped turn.
|
|
882
1048
|
const requeueInFlight = event.requeue === true;
|
|
883
|
-
void manager.interrupt(tabId, { requeueInFlight });
|
|
1049
|
+
void manager.interrupt(agentKeyFor(tabId), { requeueInFlight });
|
|
884
1050
|
bridge.push({ type: "ack", ok: true, kind: "interrupt" }, tabId);
|
|
885
1051
|
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} interrupted${requeueInFlight ? " (send-now: re-queue)" : ""}`);
|
|
886
1052
|
return;
|
|
@@ -890,7 +1056,7 @@ export async function runPanelOrchestrator() {
|
|
|
890
1056
|
if (event.type === "cancel_message" && event.tab_id) {
|
|
891
1057
|
const tabId = event.tab_id;
|
|
892
1058
|
const mid = typeof event.mid === "string" ? event.mid : undefined;
|
|
893
|
-
const removed = mid ? manager.cancelQueued(tabId, mid) : false;
|
|
1059
|
+
const removed = mid ? manager.cancelQueued(agentKeyFor(tabId), mid) : false;
|
|
894
1060
|
bridge.push({ type: "ack", ok: true, kind: "cancel_message", mid, removed }, tabId);
|
|
895
1061
|
return;
|
|
896
1062
|
}
|
|
@@ -900,7 +1066,7 @@ export async function runPanelOrchestrator() {
|
|
|
900
1066
|
const tabId = event.tab_id;
|
|
901
1067
|
// reset() is synchronous (map cleared now), so no concurrent send() can
|
|
902
1068
|
// spawn an agent before we report the cleared session.
|
|
903
|
-
manager.reset(tabId);
|
|
1069
|
+
manager.reset(agentKeyFor(tabId));
|
|
904
1070
|
bridge.push({ type: "session", session_id: null }, tabId);
|
|
905
1071
|
bridge.push({ type: "ack", ok: true, kind: "new_session" }, tabId);
|
|
906
1072
|
return;
|
|
@@ -912,7 +1078,7 @@ export async function runPanelOrchestrator() {
|
|
|
912
1078
|
if (event.type === "rewind" && event.tab_id) {
|
|
913
1079
|
const tabId = event.tab_id;
|
|
914
1080
|
const anchor = typeof event.anchor === "string" ? event.anchor : null;
|
|
915
|
-
const ok = manager.rewind(tabId, anchor);
|
|
1081
|
+
const ok = manager.rewind(agentKeyFor(tabId), anchor);
|
|
916
1082
|
bridge.push({ type: "ack", ok, kind: "rewind" }, tabId);
|
|
917
1083
|
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} rewind (anchor=${anchor ? anchor.slice(0, 8) : "fresh"}, ok=${ok})`);
|
|
918
1084
|
return;
|
|
@@ -923,7 +1089,7 @@ export async function runPanelOrchestrator() {
|
|
|
923
1089
|
const order = Array.isArray(event.order)
|
|
924
1090
|
? event.order.filter((m) => typeof m === "string")
|
|
925
1091
|
: [];
|
|
926
|
-
const ok = manager.reorderQueue(tabId, order);
|
|
1092
|
+
const ok = manager.reorderQueue(agentKeyFor(tabId), order);
|
|
927
1093
|
bridge.push({ type: "ack", ok, kind: "reorder" }, tabId);
|
|
928
1094
|
logger.info(`[panel-orchestrator] tab ${tabId.slice(0, 8)} reorder queue (${order.length} mids, ok=${ok})`);
|
|
929
1095
|
return;
|
|
@@ -934,9 +1100,10 @@ export async function runPanelOrchestrator() {
|
|
|
934
1100
|
if (event.type === "resume_session" && event.tab_id) {
|
|
935
1101
|
const tabId = event.tab_id;
|
|
936
1102
|
const sid = typeof event.session_id === "string" ? event.session_id : undefined;
|
|
937
|
-
|
|
1103
|
+
const key = agentKeyFor(tabId);
|
|
1104
|
+
manager.reset(key);
|
|
938
1105
|
if (sid)
|
|
939
|
-
manager.setResume(
|
|
1106
|
+
manager.setResume(key, sid);
|
|
940
1107
|
bridge.push({ type: "ack", ok: true, kind: "resume_session" }, tabId);
|
|
941
1108
|
return;
|
|
942
1109
|
}
|
|
@@ -1000,7 +1167,17 @@ export async function runPanelOrchestrator() {
|
|
|
1000
1167
|
catch (err) {
|
|
1001
1168
|
logger.debug(`[panel-orchestrator] queue-note check failed (ignored): ${err instanceof Error ? err.message : String(err)}`);
|
|
1002
1169
|
}
|
|
1003
|
-
|
|
1170
|
+
// Transcript replay (single-port provider switch): the panel sends the prior
|
|
1171
|
+
// conversation as `context` on the FIRST message to a freshly-switched
|
|
1172
|
+
// provider, so the new backend has the thread (minus internal session data —
|
|
1173
|
+
// thinking/tool traces/cache aren't portable across providers). Prepend it the
|
|
1174
|
+
// same way crash/queue notes are, so it seeds the fresh session's first turn.
|
|
1175
|
+
const replay = typeof event.context === "string"
|
|
1176
|
+
? event.context.trim()
|
|
1177
|
+
: "";
|
|
1178
|
+
if (replay)
|
|
1179
|
+
outText = `${replay}\n\n${outText}`;
|
|
1180
|
+
manager.send(agentKeyFor(event.tab_id), outText, {
|
|
1004
1181
|
title: event.title,
|
|
1005
1182
|
images: event.images,
|
|
1006
1183
|
mid: userMid,
|
|
@@ -1083,9 +1260,11 @@ export async function runPanelOrchestrator() {
|
|
|
1083
1260
|
QueueMonitor.stop();
|
|
1084
1261
|
unsubscribeSecrets();
|
|
1085
1262
|
await manager.stopAll();
|
|
1086
|
-
// Dispose the readiness-probe
|
|
1087
|
-
|
|
1088
|
-
|
|
1263
|
+
// Dispose the readiness-probe backends (kills each Codex/Gemini CLI child).
|
|
1264
|
+
for (const pb of probeBackends.values()) {
|
|
1265
|
+
if (pb.close)
|
|
1266
|
+
await pb.close().catch(() => { });
|
|
1267
|
+
}
|
|
1089
1268
|
// Tear down the loopback panel HTTP MCP (codex/gemini mode only).
|
|
1090
1269
|
if (panelMcpHttp)
|
|
1091
1270
|
await panelMcpHttp.stop().catch(() => { });
|