comfyui-mcp 0.52.138 → 0.52.140

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.
@@ -34,6 +34,7 @@ import { fileURLToPath } from "node:url";
34
34
  import { comfyuiFetch } from "../comfyui/fetch.js";
35
35
  import { assertPanelNotTargetedUnverifiable } from "../services/panel-pin-guard.js";
36
36
  import { findPackOnDisk, nodesInstallCommandArgs, } from "../services/node-management.js";
37
+ import { getModelInventoryDisclosure, } from "../services/model-inventory-disclosure.js";
37
38
  import { sanitizePanelUpdateNodeResult } from "../services/manager-update-error.js";
38
39
  import { formatQueueStatusPartialNote, getManifestPartialLeftover, } from "../services/manifest-partial.js";
39
40
  import { searchPanelNodes } from "../services/manager-node-search.js";
@@ -4452,6 +4453,25 @@ function withSaveAsNameConflictNote(res) {
4452
4453
  return res;
4453
4454
  return appendToolResultText(res, SAVE_AS_NAME_CONFLICT_NOTE);
4454
4455
  }
4456
+ /**
4457
+ * #2414 — a live `/models/<category>` hit does not reconcile the panel's
4458
+ * `/object_info` combo cache. Add the distinction only when both sides are
4459
+ * positively observed; an unavailable listing remains an ordinary refusal.
4460
+ */
4461
+ async function withModelInventoryDisclosure(res, widget, value, ctx) {
4462
+ if (!res.isError)
4463
+ return res;
4464
+ const probe = ctx.modelInventoryDisclosure ?? getModelInventoryDisclosure;
4465
+ try {
4466
+ const disclosure = await probe(widget, value, textOfToolResult(res));
4467
+ return disclosure ? appendToolResultText(res, disclosure) : res;
4468
+ }
4469
+ catch {
4470
+ // The disclosure is advisory. Preserve the original panel refusal if its
4471
+ // read-only corroboration fails for any reason.
4472
+ return res;
4473
+ }
4474
+ }
4455
4475
  // ---- #1695: bound the panel_set_widget previous/new echo --------------------
4456
4476
  //
4457
4477
  // Code-mode clients (Codex `functions.exec`) batch several panel_set_widget
@@ -5181,11 +5201,41 @@ function promotedEnvelopeCarriesEvidence(payload) {
5181
5201
  /** The shipped panel's only definitive non-promoted result is its own
5182
5202
  * `Node <id> (<type>) is not a subgraph` refusal. Every other graph_get_subgraph
5183
5203
  * error is an indeterminate read: a disconnect, stale route, old panel, or
5184
- * transport failure can all leave a promoted container unresolved. */
5204
+ * transport failure can all leave a promoted container unresolved.
5205
+ *
5206
+ * #2394 — the parenthesised segment is the node TYPE, and a node type may itself
5207
+ * contain parentheses: rgthree names every node that way (`Power Lora Loader
5208
+ * (rgthree)`), as do several other packs (`KSampler (Efficient)`). A `[^)]*`
5209
+ * body stops at the FIRST `)`, so `Node 82 (Power Lora Loader (rgthree)) is not
5210
+ * a subgraph` did not match its own definitive message. The read was then
5211
+ * treated as indeterminate and the write refused with "graph_get_subgraph could
5212
+ * not determine whether the addressed node is a promoted container" — the exact
5213
+ * refusal #2394 reported, on a node that is provably ordinary.
5214
+ *
5215
+ * The type body is matched as a BALANCED group (plain text, or text containing
5216
+ * one parenthesised group) UNIONED with the original `[^)]*` branch. Both halves
5217
+ * of that union are load-bearing, and each was added because a simpler edit
5218
+ * regressed a case that already worked (both caught by the codex gate):
5219
+ *
5220
+ * - Widening to `.*` looks equivalent but is not: `.` does not cross a newline
5221
+ * and `[^)]*` did, so a node type carrying a newline stopped matching.
5222
+ * - The balanced branch alone rejects an UNMATCHED `(` — a type like
5223
+ * `Power (rgthree` yields `Node 82 (Power (rgthree) is not a subgraph`, which
5224
+ * the original matched by stopping at the first `)`.
5225
+ *
5226
+ * Keeping the original branch in the union makes the accept set a strict
5227
+ * superset of the original's, so no message that was definitive before can
5228
+ * become indeterminate now. The reject set is unchanged: still anchored at
5229
+ * `^Error: Node `, still requiring the literal ` is not a subgraph` tail. A type
5230
+ * nesting parentheses more than one level deep still does not match and is
5231
+ * refused as indeterminate — the fail-closed direction, which is the correct
5232
+ * default for a predicate whose `true` authorizes an ordinary write. The
5233
+ * `[canvas-root-divergence]` diagnosis never begins with `Error: Node `, so it
5234
+ * cannot collide. */
5185
5235
  function isDefinitiveNonPromotedSubgraphRead(res) {
5186
5236
  if (!res.isError)
5187
5237
  return false;
5188
- return /^Error:\s*Node\s+\S+(?:\s+\([^)]*\))?\s+is not a subgraph\b/i.test(textOfToolResult(res));
5238
+ return /^Error:\s*Node\s+\S+(?:\s+\((?:(?:[^()]|\([^()]*\))*|[^)]*)\))?\s+is not a subgraph\b/i.test(textOfToolResult(res));
5189
5239
  }
5190
5240
  /**
5191
5241
  * panel#1869 — the panel's `[canvas-root-divergence]` refusal is a DIAGNOSIS,
@@ -5564,6 +5614,41 @@ function promotedPanelBuildRefusal(ctx, nodeId, widget, reason, capability) {
5564
5614
  * indeterminate read fails closed because the panel can resolve a promoted
5565
5615
  * container to its unsafe inner node.
5566
5616
  */
5617
+ /**
5618
+ * Why the panel binding is no longer the one we captured, or undefined when it is.
5619
+ *
5620
+ * WHY THIS IS A FUNCTION AND NOT A THIRD COPY. An early `return null` out of the
5621
+ * promoted pre-dispatch check does not mean "refuse" — it means "this is not a promoted
5622
+ * write, proceed on the ordinary path". So every exit that crosses an `await` is a WRITE
5623
+ * AUTHORIZATION issued against a binding that may have moved underneath it, and each one
5624
+ * needs the same re-read. There were two hand-written copies of these five checks; a third
5625
+ * exit was then added between them without one, which is #2401, and fixing that exit alone
5626
+ * left the next one bare, which is #2409. One copy, called by each exit, so the next early
5627
+ * return has one obvious thing to call.
5628
+ *
5629
+ * `where` is appended to each message so the caller keeps naming its own await.
5630
+ */
5631
+ function panelBindingDriftReason(ctx, where, hasIdentityApi, identityBefore, tabBefore) {
5632
+ if (!hasIdentityApi)
5633
+ return `the receiver identity was unavailable ${where}`;
5634
+ if (!isUsablePanelConnectionIdentity(identityBefore)) {
5635
+ return `the panel connection identity was unavailable ${where}`;
5636
+ }
5637
+ let identityAfter;
5638
+ try {
5639
+ identityAfter = ctx.panelConnectionIdentity?.();
5640
+ }
5641
+ catch {
5642
+ return `the panel connection identity became unreadable ${where}`;
5643
+ }
5644
+ if (ctx.tabId !== tabBefore || !isUsablePanelConnectionIdentity(identityAfter)) {
5645
+ return `the panel tab or connection changed ${where}`;
5646
+ }
5647
+ if (!samePanelConnectionIdentity(identityBefore, identityAfter)) {
5648
+ return `the panel session or connection changed ${where}`;
5649
+ }
5650
+ return undefined;
5651
+ }
5567
5652
  async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
5568
5653
  // Production PanelToolCtx is always backed by UiBridge, which exposes this
5569
5654
  // per-hello capability getter. A few transport-only forwarding contexts (and
@@ -5600,12 +5685,23 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
5600
5685
  // absent. An unreadable scope probe is not permission for an outer write;
5601
5686
  // it falls through to the existing conservative graph_get_subgraph path.
5602
5687
  const targetScope = await readPromotedTargetScope(ctx, nodeId);
5603
- if (targetScope?.activeView === "root" && targetScope.node === "ordinary")
5688
+ if (targetScope?.activeView === "root" && targetScope.node === "ordinary") {
5689
+ // The scope probe crossed an await. Re-read the binding before taking the
5690
+ // ordinary fast path; otherwise a tab/connection rebind can make this
5691
+ // probe's ordinary classification authorize a write on the new receiver.
5692
+ const drift = panelBindingDriftReason(ctx, "after the scope probe", hasIdentityApi, identityBefore, tabBefore);
5693
+ if (drift)
5694
+ return promotedWriteRefusal(widget, drift);
5604
5695
  return null;
5696
+ }
5605
5697
  const sub = await ctx.call({ cmd: "graph_get_subgraph", node_id: nodeId });
5606
5698
  if (sub.isError) {
5607
- if (isDefinitiveNonPromotedSubgraphRead(sub))
5699
+ if (isDefinitiveNonPromotedSubgraphRead(sub)) {
5700
+ const drift2 = panelBindingDriftReason(ctx, "after the subgraph read", hasIdentityApi, identityBefore, tabBefore);
5701
+ if (drift2)
5702
+ return promotedWriteRefusal(widget, drift2);
5608
5703
  return null;
5704
+ }
5609
5705
  return promotedSubgraphReadRefusal(widget, sub, "graph_get_subgraph could not determine whether the addressed node is a promoted container");
5610
5706
  }
5611
5707
  let payload = parseToolResultJson(sub);
@@ -8522,6 +8618,7 @@ async function rebindWorkflowFence(ctx, opts) {
8522
8618
  before,
8523
8619
  kind: "no_uuid",
8524
8620
  why: "the active workflow record carries no usable workflow_uuid",
8621
+ active,
8525
8622
  };
8526
8623
  }
8527
8624
  // "already current" requires a KNOWN prior fence equal to the live uuid. An
@@ -9161,8 +9258,19 @@ WHAT TO DO: settle it with a graph read — call panel_graph_outline. That ` +
9161
9258
  * active object and accept the returned UUID only while it still names this
9162
9259
  * open's canonical target; otherwise leave the old stamp to fail closed.
9163
9260
  */
9164
- async function refreshOpenWorkflowUuid(ctx, requestedPath, openResult) {
9261
+ async function refreshOpenWorkflowUuid(ctx, requestedPath, openResult, legacyRebind) {
9165
9262
  const parsedOpen = parseToolResultJson(openResult);
9263
+ // #971 compatibility: older/lightweight bridges may return only an explicit
9264
+ // transport route for a successful open. The proof is supplied by the one
9265
+ // immediately following open only after the caller consumed it before any
9266
+ // await; it is never a substitute for the modern bare-alias `opened.path` +
9267
+ // confirmed active workflow contract.
9268
+ const legacyReboundRoute = legacyRebind?.tabId === ctx.tabId &&
9269
+ legacyRebind.savedIdentity === canonicalBareSavedIdentity(requestedPath) &&
9270
+ !openResult.isError &&
9271
+ parsedOpen?.ok === true &&
9272
+ parsedOpen.routedTo === ctx.tabId &&
9273
+ (typeof ctx.bridge.canReach !== "function" || ctx.bridge.canReach(ctx.tabId));
9166
9274
  const opened = parsedOpen?.opened;
9167
9275
  const openedPath = opened && typeof opened === "object" && typeof opened.path === "string"
9168
9276
  ? opened.path
@@ -9171,11 +9279,118 @@ async function refreshOpenWorkflowUuid(ctx, requestedPath, openResult) {
9171
9279
  // alias/basename to a path, but that reply must never retroactively turn the
9172
9280
  // alias into a UUID-refresh authorization. Require the reply to corroborate
9173
9281
  // the original exact saved identity before consulting the live active record.
9282
+ const requestedSavedPath = canonicalSavedWorkflowPath(requestedPath);
9283
+ const requestedIsBareAlias = !!requestedSavedPath && !requestedSavedPath.includes("/");
9174
9284
  const requestedIdentity = canonicalRequestedSavedIdentity(requestedPath);
9175
9285
  const openedIdentity = openedPath
9176
9286
  ? canonicalSavedRecordIdentity({ path: openedPath, routing_key: parsedOpen?.routing_key })
9177
9287
  : null;
9288
+ if (requestedIsBareAlias) {
9289
+ // A bare filename is only a selector. It is not safe to let a native
9290
+ // success through unless the panel both resolved it to a saved path and
9291
+ // gave us a fresh, explicitly confirmed active observation. In particular,
9292
+ // an omitted `opened.path` is not a resolution, and an omitted
9293
+ // `active_confirmed` is not confirmation (#1639).
9294
+ if (!openedPath) {
9295
+ if (legacyReboundRoute)
9296
+ return null;
9297
+ return {
9298
+ drifted: true,
9299
+ unverified: true,
9300
+ activeLabel: "the panel did not return a resolved path for this filename",
9301
+ };
9302
+ }
9303
+ if (!canonicalRequestedSavedIdentity(openedPath)) {
9304
+ return {
9305
+ drifted: true,
9306
+ unverified: true,
9307
+ activeLabel: "the panel did not return a resolved path for this filename",
9308
+ };
9309
+ }
9310
+ let list = null;
9311
+ try {
9312
+ const res = await ctx.call({ cmd: "workflow_list" }, 6000);
9313
+ if (!res?.isError)
9314
+ list = parseToolResultJson(res);
9315
+ }
9316
+ catch {
9317
+ list = null;
9318
+ }
9319
+ if (!list || list.active_confirmed !== true) {
9320
+ return {
9321
+ drifted: true,
9322
+ unverified: true,
9323
+ activeLabel: "the panel did not return a freshly confirmed active workflow after resolving this filename",
9324
+ };
9325
+ }
9326
+ const resolvedIdentity = canonicalSavedRecordIdentity({
9327
+ path: openedPath,
9328
+ routing_key: parsedOpen?.routing_key,
9329
+ });
9330
+ const activeIdentity = canonicalSavedRecordIdentity(list.active);
9331
+ if (resolvedIdentity && activeIdentity === resolvedIdentity) {
9332
+ // The re-read proves which resolved path is active, but the caller only
9333
+ // supplied an alias. Preserve #716's no-adoption rule.
9334
+ return null;
9335
+ }
9336
+ if (activeIdentity) {
9337
+ return { drifted: true, activeLabel: describeActiveRecord(list.active) };
9338
+ }
9339
+ return {
9340
+ drifted: true,
9341
+ unverified: true,
9342
+ activeLabel: "the panel returned an unconfirmed active workflow after resolving this filename",
9343
+ };
9344
+ }
9178
9345
  if (!requestedIdentity || requestedIdentity !== openedIdentity) {
9346
+ // A bare filename is an alias, not a saved identity. The panel's `opened.path`
9347
+ // is the resolution of that alias; once it is available, corroborate THAT exact
9348
+ // path against a fresh active-list read before allowing the caller to treat the
9349
+ // success as an active-canvas success. Without this branch, the alias exits here
9350
+ // before any live observation and a stale previous tab can be reported as active
9351
+ // and bound (#1639).
9352
+ const resolvedIdentity = openedPath ? canonicalRequestedSavedIdentity(openedPath) : null;
9353
+ if (!requestedIdentity && resolvedIdentity) {
9354
+ let list = null;
9355
+ try {
9356
+ const res = await ctx.call({ cmd: "workflow_list" }, 6000);
9357
+ if (!res?.isError)
9358
+ list = parseToolResultJson(res);
9359
+ }
9360
+ catch {
9361
+ list = null;
9362
+ }
9363
+ if (!list) {
9364
+ return {
9365
+ drifted: true,
9366
+ unverified: true,
9367
+ activeLabel: "the panel did not return a confirmed active workflow after resolving this filename",
9368
+ };
9369
+ }
9370
+ if (list.active_confirmed !== true) {
9371
+ return {
9372
+ drifted: true,
9373
+ unverified: true,
9374
+ activeLabel: "the panel returned an unconfirmed active workflow after resolving this filename",
9375
+ };
9376
+ }
9377
+ const activeIdentity = canonicalSavedRecordIdentity(list.active);
9378
+ if (activeIdentity === resolvedIdentity) {
9379
+ // The re-read proves which resolved path is active, but the caller only
9380
+ // supplied an alias. Preserve #716's no-adoption rule: an alias may be
9381
+ // observed for the #1639 wrong-canvas guard, never promoted into a new
9382
+ // command-fence UUID by the reply-resolved path.
9383
+ return null;
9384
+ }
9385
+ if (activeIdentity) {
9386
+ return { drifted: true, activeLabel: describeActiveRecord(list.active) };
9387
+ }
9388
+ return {
9389
+ drifted: true,
9390
+ unverified: true,
9391
+ activeLabel: "the panel returned an unconfirmed active workflow after resolving this filename",
9392
+ };
9393
+ }
9179
9394
  // #812 — the SAVED corroboration above can never succeed for an unsaved
9180
9395
  // target (there is no path), so try the parallel UNSAVED identity: the
9181
9396
  // caller's literal token against the panel's own proven routing_key for
@@ -9413,6 +9628,11 @@ function noReachableTabFail(cmd, ctx) {
9413
9628
  `sent. Retry in a moment, or rebind with panel_set_workflow_target({mode:"current"}).`);
9414
9629
  }
9415
9630
  async function openWorkflowWithVerify(path, ctx) {
9631
+ // #971 — consume the explicit-current proof BEFORE the first await. This
9632
+ // makes it one-shot even when this open fails, and prevents a concurrent or
9633
+ // later open from inheriting a proof belonging to an earlier operation.
9634
+ const legacyRebind = ctx.lastExplicitCurrentRebind;
9635
+ ctx.lastExplicitCurrentRebind = undefined;
9416
9636
  // #402: after a full ComfyUI restart the browser tab re-registers a few seconds
9417
9637
  // later. Awaiting a stable binding BEFORE dispatching a mutating workflow_open
9418
9638
  // (nothing is sent yet — no double-apply risk) means the command reaches a live
@@ -9476,7 +9696,7 @@ async function openWorkflowWithVerify(path, ctx) {
9476
9696
  `save-as, or edit expecting ${path}. Call panel_list_workflows to see the current state.`);
9477
9697
  }
9478
9698
  {
9479
- const drift = await refreshOpenWorkflowUuid(ctx, path, res);
9699
+ const drift = await refreshOpenWorkflowUuid(ctx, path, res, legacyRebind);
9480
9700
  // #887 — the read above is the ONLY observation in this whole path taken
9481
9701
  // after a real round trip, so it is the only thing that can catch the active
9482
9702
  // pointer having settled elsewhere. It already declined to adopt the uuid;
@@ -9489,6 +9709,12 @@ async function openWorkflowWithVerify(path, ctx) {
9489
9709
  // act is typically a write. The session's fence is untouched (nothing was
9490
9710
  // adopted), so the tab that IS active keeps its own protection.
9491
9711
  if (drift) {
9712
+ if (drift.unverified) {
9713
+ return fail(`workflow_open: ${path} was reported applied, but the panel could not prove that ` +
9714
+ `${drift.activeLabel}. The active canvas is UNKNOWN, so this open is not a ` +
9715
+ `success and this session's workflow identity was NOT re-pointed. Inspect ` +
9716
+ `panel_list_workflows before reading or writing the graph.`);
9717
+ }
9492
9718
  return fail(`workflow_open: ${path} was opened, but ${drift.activeLabel} is the ACTIVE workflow now — ` +
9493
9719
  `the panel confirmed this on a re-read after the open completed. This session's ` +
9494
9720
  `workflow identity was NOT re-pointed at ${path}, so the canvas you would read or ` +
@@ -9829,6 +10055,11 @@ function canonicalRequestedSavedIdentity(path) {
9829
10055
  // authorize replacing its existing UUID stamp.
9830
10056
  return canonicalPath && canonicalPath.includes("/") ? `wf:${canonicalPath}` : null;
9831
10057
  }
10058
+ /** Canonical identity for a bare saved-workflow selector, for one-shot #971 proof matching. */
10059
+ function canonicalBareSavedIdentity(path) {
10060
+ const canonicalPath = canonicalSavedWorkflowPath(path);
10061
+ return canonicalPath && !canonicalPath.includes("/") ? `wf:${canonicalPath}` : null;
10062
+ }
9832
10063
  /**
9833
10064
  * Canonical `wf:<path>` identity, but only for a complete corroborating record.
9834
10065
  * Command-fence replacement is stricter than pin routing: a same-path record
@@ -14918,8 +15149,9 @@ export function buildPanelToolDefs() {
14918
15149
  // inner mapping and set it there (the issue's own workaround), then
14919
15150
  // leave the subgraph so the caller's scope is unchanged.
14920
15151
  const refusal = parseContradictoryPromotedWidgetRefusal(textOfToolResult(first), args.widget);
14921
- if (!refusal || String(refusal.nodeId) !== String(args.node_id))
14922
- return first;
15152
+ if (!refusal || String(refusal.nodeId) !== String(args.node_id)) {
15153
+ return withModelInventoryDisclosure(first, args.widget, value, ctx);
15154
+ }
14923
15155
  if (refusal.widget !== args.widget) {
14924
15156
  const remapped = await guardedWrite(args.node_id, refusal.widget);
14925
15157
  if (!remapped.isError) {
@@ -17087,6 +17319,10 @@ export function buildPanelToolDefs() {
17087
17319
  if (mode === "pinned" && !(path ?? "").trim()) {
17088
17320
  return fail("Provide path when pinning — use panel_list_workflows to list open workflows.");
17089
17321
  }
17322
+ // A prior explicit recovery proof must not survive another targeting
17323
+ // call. It is valid only for the open immediately following the rebind
17324
+ // that produced it.
17325
+ ctx.lastExplicitCurrentRebind = undefined;
17090
17326
  // mode:'current' is the explicit, user/agent-initiated "rebind me to the
17091
17327
  // tab that's live now" consent signal. Self-heal a session whose captured
17092
17328
  // tab id was orphaned (reconnect/reload/workflow-switch) BEFORE writing the
@@ -17099,8 +17335,10 @@ export function buildPanelToolDefs() {
17099
17335
  // on a successful turn-pin recovery. Track that separately from the
17100
17336
  // real-tab rebind note below.
17101
17337
  let currentModeTurnRepinned = false;
17338
+ let explicitCurrentRebindBefore;
17102
17339
  if (mode === "current" && ctx.rebindToActiveTab) {
17103
17340
  const before = ctx.tabId;
17341
+ explicitCurrentRebindBefore = before;
17104
17342
  const recoveringScope = isScopeAddress(before);
17105
17343
  // Hold the send() wait BEFORE the first await so a same-batch sibling
17106
17344
  // that already hit the null pin waits instead of minting #884.
@@ -17360,6 +17598,21 @@ export function buildPanelToolDefs() {
17360
17598
  rebindNote += ` The panel dropped mid-call: this session moved from tab ${tabBeforeProbe} onto tab ${ctx.tabId}, and mode:"current" was applied there too.`;
17361
17599
  }
17362
17600
  }
17601
+ // #971 — preserve the old-panel recovery only when the current-mode
17602
+ // operation itself corroborated the saved identity now selected. The
17603
+ // proof is consumed by the next open before its first await, so a failed
17604
+ // open, a different alias, or a later open cannot inherit it.
17605
+ if (mode === "current" &&
17606
+ !deferredBind &&
17607
+ explicitCurrentRebindBefore !== undefined &&
17608
+ ctx.tabId !== explicitCurrentRebindBefore &&
17609
+ fenceRebind &&
17610
+ "active" in fenceRebind) {
17611
+ const savedIdentity = canonicalSavedRecordIdentity(fenceRebind.active);
17612
+ if (savedIdentity) {
17613
+ ctx.lastExplicitCurrentRebind = { tabId: ctx.tabId, savedIdentity };
17614
+ }
17615
+ }
17363
17616
  // panel#1529 — "Graph tools will target that workflow" is a CLAIM ABOUT
17364
17617
  // ROUTING, and this call only ever wrote the workflow-target store. It is
17365
17618
  // true when the panel confirmed the target IS the live canvas, because a
@@ -19535,7 +19788,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
19535
19788
  // and refuse to report freed:true when a device is still pinned.
19536
19789
  return annotateFreeVramAck(ctx, res);
19537
19790
  }),
19538
- def("panel_show_media", "Display one or more images, videos or AUDIO files directly in the panel chat. Use this whenever the user asks to SEE, SHOW, PLAY or HEAR a file — a disk path you composited/downloaded/generated (absolute path on the orchestrator host) OR a ComfyUI output ref ({ filename, subfolder?, type? }). Items are rendered as media cards in the agent chat area — audio gets a real player (.wav/.mp3/.flac/.ogg/.oga/.opus/.m4a/.aac), so a generated TTS or voice take is played back with this tool, not described; supply optional captions. You are never sent the audio yourself and cannot hear it. Max 8 items per call. A path item OVER the 20 MB inline cap that is not under any directory ComfyUI serves can still be shown by passing stage:true on that item — the orchestrator COPIES it into <output>/_panel_staged (an opt-in, persistent disk write; 512 MB per-file and 2 GB total caps) and displays the copy by reference. NEVER describe an image with emoji or text placeholders — call this tool instead.", {
19791
+ def("panel_show_media", "Display one or more images, videos or AUDIO files directly in the panel chat. Use this whenever the user asks to SEE, SHOW, PLAY or HEAR a file — a disk path you composited/downloaded/generated (absolute path on the orchestrator host) OR a ComfyUI output ref ({ filename, subfolder?, type? }). Items are rendered as media cards in the agent chat area — audio gets a real player (.wav/.mp3/.flac/.ogg/.oga/.opus/.m4a/.aac), so a generated TTS or voice take is played back with this tool, not described; supply optional captions. You are never sent the audio yourself and cannot hear it. Max 8 items per call. A path item OVER the 20 MB inline cap that is not under any directory ComfyUI serves can still be shown by passing stage:true on that item — the orchestrator COPIES it into <output>/_panel_staged (an opt-in, persistent disk write; 512 MB per-file and 2 GB total caps) and displays the copy by reference. Staging needs a LOCAL ComfyUI: it is a filesystem copy into ComfyUI's own output directory, so it cannot run against a remote target (--comfyui-url on another host) and the call reports that instead of staging. NEVER describe an image with emoji or text placeholders — call this tool instead.", {
19539
19792
  items: z
19540
19793
  .array(z.object({
19541
19794
  // #1968 — OPTIONAL in the shape, required by normalizeShowMediaItem