comfyui-mcp 0.52.115 → 0.52.116
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/panel-tools.js +184 -28
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/kitchen.js +33 -2
- package/dist/services/kitchen.js.map +1 -1
- package/dist/services/process-control.js +250 -22
- package/dist/services/process-control.js.map +1 -1
- package/package.json +1 -1
|
@@ -191,7 +191,7 @@ import { RunCompletions } from "./run-completion-journal.js";
|
|
|
191
191
|
import { AskAnswers, askFingerprint, PANEL_ASK_ID_PREFIX, } from "./ask-answer-journal.js";
|
|
192
192
|
import { getQueueVerified, resetClient, resetObjectInfoCache, } from "../comfyui/client.js";
|
|
193
193
|
import { convertUiToApi, collectNodeTypes } from "../services/workflow-converter.js";
|
|
194
|
-
import { restartComfyUI, preflightLocalRestart, readServingArgv, resolveVerifiedProxyRestartTarget, describeArgvDrift, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, } from "../services/process-control.js";
|
|
194
|
+
import { captureComfyUITargetFence, restartComfyUI, preflightLocalRestart, readServingArgv, resolveVerifiedProxyRestartTarget, describeArgvDrift, recordRestartDispatch, clearRestartDispatch, getRestartDispatchRecord, RESTART_DISPATCH_CAUSATION_WINDOW_MS, PROCESS_WIDE_RESTART_DISPATCH_TOKEN, __processControlTestHooks, targetFenceMatchesCurrent, targetFencesEqual, } from "../services/process-control.js";
|
|
195
195
|
import { resetManagerApiCache } from "../services/manager-api-cache.js";
|
|
196
196
|
import { desktopSavedLaunchArgs, describeSavedLaunchArgDrift, } from "../services/desktop-launch-args.js";
|
|
197
197
|
import { config, isRemoteMode, isCloudMode, getBootLocalComfyUIBaseUrl, getComfyUIBaseUrl, getComfyuiTargetGeneration, } from "../config.js";
|
|
@@ -10013,7 +10013,7 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
10013
10013
|
const routed = target ? withWorkflowTarget(cmd, target) : cmd;
|
|
10014
10014
|
return bridge.send(routed, { tabId: ctx.tabId, timeoutMs, onDispatchedRid });
|
|
10015
10015
|
};
|
|
10016
|
-
const callOnce = async (cmd, timeoutMs, onDispatchedRid,
|
|
10016
|
+
const callOnce = async (cmd, timeoutMs, onDispatchedRid, beforeDispatch,
|
|
10017
10017
|
// #1560 — reports the error each attempt failed on, so the wrapper below can
|
|
10018
10018
|
// stamp the bridge's structural marks onto the ToolResult from ONE place. The
|
|
10019
10019
|
// catch has more than a dozen `fail(...)` exits, each shaping its own message;
|
|
@@ -10055,6 +10055,10 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
10055
10055
|
const blocked = graphCmdBlockedByRunningPrompt(cmd);
|
|
10056
10056
|
if (blocked)
|
|
10057
10057
|
return fail(blocked);
|
|
10058
|
+
// A caller may hold a target/generation fence across the reachability wait.
|
|
10059
|
+
// Run it synchronously in the final gap before sendRouted: an async check here
|
|
10060
|
+
// would reopen the exact retarget window this guard closes.
|
|
10061
|
+
beforeDispatch?.();
|
|
10058
10062
|
const firstTry = ok(await sendRouted(cmd, timeoutMs, observeRid));
|
|
10059
10063
|
// panel#1097 — a guard-domain command that SUCCEEDS is the evidence that the
|
|
10060
10064
|
// switch is over, whichever attempt lands it. Without this an ordinary
|
|
@@ -10138,6 +10142,7 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
10138
10142
|
await awaitReachable();
|
|
10139
10143
|
ensureReachable(); // rebinds a current-mode session onto the reconnected tab
|
|
10140
10144
|
holdTab = ctx.tabId;
|
|
10145
|
+
beforeDispatch?.();
|
|
10141
10146
|
const retried = ok(await sendRouted(cmd, timeoutMs, observeRid));
|
|
10142
10147
|
// Cleared HERE and nowhere else, and only when the failure this retried
|
|
10143
10148
|
// was a SWITCH refusal. Clearing on every routed success let an unguarded
|
|
@@ -10921,9 +10926,9 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
10921
10926
|
* concurrent `ctx.call`s — including the `rebindWorkflowFence` round trip that
|
|
10922
10927
|
* `callOnce`'s own catch makes — cannot read each other's.
|
|
10923
10928
|
*/
|
|
10924
|
-
const call = async (cmd, timeoutMs, onDispatchedRid) => {
|
|
10929
|
+
const call = async (cmd, timeoutMs, onDispatchedRid, beforeDispatch) => {
|
|
10925
10930
|
let failure;
|
|
10926
|
-
const res = await callOnce(cmd, timeoutMs, onDispatchedRid, (err) => {
|
|
10931
|
+
const res = await callOnce(cmd, timeoutMs, onDispatchedRid, beforeDispatch, (err) => {
|
|
10927
10932
|
failure = err;
|
|
10928
10933
|
});
|
|
10929
10934
|
return carryWorkflowListReadinessMark(failure, carryMidCommandDisconnectMark(failure, carryPanelAnsweredMark(failure, res)));
|
|
@@ -12606,7 +12611,7 @@ function withRetryToken(d) {
|
|
|
12606
12611
|
// Double-apply is already the #521 panel ledger's job; this only makes the
|
|
12607
12612
|
// outcome VISIBLE, which is the half of #694 nothing else does.
|
|
12608
12613
|
const wrapped = Object.create(ctx);
|
|
12609
|
-
wrapped.call = (cmd, timeoutMs, onDispatchedRid) => ctx.call(
|
|
12614
|
+
wrapped.call = (cmd, timeoutMs, onDispatchedRid, beforeDispatch) => ctx.call(
|
|
12610
12615
|
// Ask the RETRY MAP's question, not the workflow fence's (codex gate).
|
|
12611
12616
|
// These were the same answer only while isMutatingGraphCommand
|
|
12612
12617
|
// over-classified — the #778 defect. Once the fence got its own effect
|
|
@@ -12622,7 +12627,7 @@ function withRetryToken(d) {
|
|
|
12622
12627
|
// reads, which is asserted in panel-retry-identity.test.ts.
|
|
12623
12628
|
RETRY_TOKEN_CMDS.has(typeof cmd.cmd === "string" ? cmd.cmd : "")
|
|
12624
12629
|
? { ...cmd, retry_of: retryOf }
|
|
12625
|
-
: cmd, timeoutMs, onDispatchedRid);
|
|
12630
|
+
: cmd, timeoutMs, onDispatchedRid, beforeDispatch);
|
|
12626
12631
|
const out = await d.handler(args, wrapped);
|
|
12627
12632
|
// Drained AFTER the handler, deliberately, for two measured reasons.
|
|
12628
12633
|
//
|
|
@@ -12676,6 +12681,44 @@ function withRetryToken(d) {
|
|
|
12676
12681
|
export function buildPanelToolDefs() {
|
|
12677
12682
|
// Local helper so each def reads like the original `tool(...)` call.
|
|
12678
12683
|
const def = (name, description, schema, handler) => ({ name, description, schema, handler });
|
|
12684
|
+
const capturePanelKitchenTarget = (ctx) => {
|
|
12685
|
+
const base = getComfyUIBaseUrl().replace(/\/+$/, "");
|
|
12686
|
+
const observed = ctx.bridge.tabServerOrigin?.(ctx.tabId);
|
|
12687
|
+
// A hello.comfyui_url/tabOrigin value is page-JS-writable and cannot authorize
|
|
12688
|
+
// a process mutation. The server-observed handshake proves only the origin,
|
|
12689
|
+
// not a path mount; refuse mounted targets rather than guessing which instance
|
|
12690
|
+
// this tab fronts. This is the same fail-closed rule used by panel_reboot.
|
|
12691
|
+
if (ctx.bridge.tabIsLocal?.(ctx.tabId) !== true ||
|
|
12692
|
+
!observed ||
|
|
12693
|
+
!sameHttpOrigin(observed, base) ||
|
|
12694
|
+
(() => {
|
|
12695
|
+
try {
|
|
12696
|
+
return new URL(base).pathname.replace(/\/+$/, "") !== "";
|
|
12697
|
+
}
|
|
12698
|
+
catch {
|
|
12699
|
+
return true;
|
|
12700
|
+
}
|
|
12701
|
+
})()) {
|
|
12702
|
+
return undefined;
|
|
12703
|
+
}
|
|
12704
|
+
return { fence: captureComfyUITargetFence(), tabId: ctx.tabId };
|
|
12705
|
+
};
|
|
12706
|
+
const panelKitchenTargetStillCurrent = (ctx, target) => {
|
|
12707
|
+
if (ctx.tabId !== target.tabId || !targetFenceMatchesCurrent(target.fence))
|
|
12708
|
+
return false;
|
|
12709
|
+
const observed = ctx.bridge.tabServerOrigin?.(target.tabId);
|
|
12710
|
+
return (ctx.bridge.tabIsLocal?.(target.tabId) === true &&
|
|
12711
|
+
observed != null &&
|
|
12712
|
+
sameHttpOrigin(observed, target.fence.baseUrl) &&
|
|
12713
|
+
(() => {
|
|
12714
|
+
try {
|
|
12715
|
+
return new URL(target.fence.baseUrl).pathname.replace(/\/+$/, "") === "";
|
|
12716
|
+
}
|
|
12717
|
+
catch {
|
|
12718
|
+
return false;
|
|
12719
|
+
}
|
|
12720
|
+
})());
|
|
12721
|
+
};
|
|
12679
12722
|
const defs = [
|
|
12680
12723
|
def("panel_query_graph", "FILTER or TRAVERSE a SUBSET of the live canvas, for when you ALREADY KNOW what you're looking for. NOT for 'show me the canvas' or any whole-graph overview — call panel_graph_outline FIRST for that. NOT get_workflow's query action (that queries a saved file or JSON you provide, not the live canvas). Filters, traverses, projects and aggregates over the workflow the user is CURRENTLY VIEWING without dumping the whole graph (replaces the old panel_get_graph full-JSON dump; output is TOKEN-BOUNDED with an explicit truncation marker, so a big graph can never flood your context). Combine: `types` (node type contains any), `title` (contains), `where` widget predicates ANDed ('cfg>7', 'steps<=20', 'sampler_name=euler', 'text~sunset' — ops = != >= <= > < ~contains), `ids` (exact nodes — THE way to read ONE node's exact slot/widget detail: {ids:[42], fields:'detail'}), `upstream_of`/`downstream_of` + `depth` (dependency traversal: upstream = what FEEDS that node, downstream = what CONSUMES it; seed at depth 0), `fields` ('compact' one line per node [default], 'ids', 'detail' = the full node summary with slots + connections + mode), `group_by:'type'` (counts only), `limit` (default 40). detail rows include each node's MODE — a 'bypass' node is skipped and a 'mute' node kills everything downstream, so check modes on the path you care about before running (fix with panel_set_node_mode). Every result also carries `groups` (id, title, member node_ids — groups are geometric, trust this list) and, when viewing a SUBGRAPH (after panel_enter_subgraph), `rails` (boundary rail ids/slots). `max_chars` bounds the WHOLE result, those riders included, and the rows you asked for are spent first: on a big graph the riders lose their member ids, then drop out entirely, rather than starving your query — and each says in-band when it did, with the true counts. `widget_max_chars` raises the per-widget cap only for `fields:'detail'`; use it only with exactly one explicit `ids` entry (for example `{ids:[42], fields:'detail', widget_max_chars:8192}`), with a default of 2048 and a maximum of 32768. If supplied without those preconditions, it is not sent and the result says why; a legacy Panel that still returns the default cap is named in the result. It does not change compact, ids, or broad reads. Typical flow: panel_graph_outline to orient → panel_query_graph to pinpoint/inspect → edit. Read-only.", {
|
|
12681
12724
|
types: z.array(z.string()).optional().describe("Node type contains ANY of these (case-insensitive)."),
|
|
@@ -12749,6 +12792,10 @@ export function buildPanelToolDefs() {
|
|
|
12749
12792
|
});
|
|
12750
12793
|
return fitQueryGraphReply(panelReply, args.max_chars, widgetMaxChars.note ?? legacyWidgetMaxCharsNote(panelReply, widgetMaxChars.value));
|
|
12751
12794
|
}),
|
|
12795
|
+
// panel_kitchen mixes panel-routed graph evidence with process-global
|
|
12796
|
+
// ComfyUI probes. Bind both halves to the same tab target before allowing
|
|
12797
|
+
// an assessment or mutation; a concurrent hello from another tab must
|
|
12798
|
+
// produce a refusal, never a cross-instance apply.
|
|
12752
12799
|
def("panel_graph_outline", "READ THE LIVE CANVAS the user is looking at, as text. 'Show me what's on the canvas' / 'what's on the graph right now' / 'read the current workflow' / 'describe the open graph' -> THIS TOOL, with no arguments. NOT visualize_workflow (it DRAWS A DIAGRAM of a workflow you PASS IN — a saved file or JSON — and never sees the live canvas). NOT panel_query_graph (that FILTERS a SUBSET, for when you already know what you're looking for). Returns one `outline` string covering the WHOLE open graph, topologically sorted (sources first, sinks last): each node as `id Type \"title\" [bypass/mute] [OUTPUT] · group:X widget=value …` with `← inputs` (source_node.output_name) and `→ outputs` (target_node.input_name), after a GROUPS index (title → member node ids). It gives you the WIRING you would otherwise reconstruct by hand — read it FIRST to get oriented, then panel_query_graph to inspect one node ({ids:[42], fields:'detail'}) or panel_find_nodes for free-text search. Over `max_chars` it never cuts the graph short: it sheds per-node detail, or refuses with a reason — never a partial outline. Read-only.", {
|
|
12753
12800
|
max_chars: z
|
|
12754
12801
|
.number()
|
|
@@ -18499,7 +18546,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
18499
18546
|
def("panel_kitchen", "See what comfy-kitchen can do on this GPU against the OPEN canvas, find where the live graph leaves it on the table, and apply the faster path. Same actions as the core `kitchen` tool, fenced like every other panel mutation. Driven by `action`:\n" +
|
|
18500
18547
|
'- action:"status" — kitchen version, backends (hip/cuda/triton/eager), INT8 attention, GPU fp8/NVFP4/MXFP8, launch flags. Remote sessions report the import probe and model.quant as unknown.\n' +
|
|
18501
18548
|
'- action:"assess" — walk the live graph\'s UNETLoaders. A recommendation fires only when every fact it needs is known (fp8_e4m3fn_fast widget; --use-ck-attention flag; NVFP4 swap; ROCm --enable-triton-backend).\n' +
|
|
18502
|
-
'- action:"apply" — apply one recommendation_id. Widget edits go through graph_set_widget (Ctrl+Z). Flags need confirm:true;
|
|
18549
|
+
'- action:"apply" — apply one recommendation_id. Widget edits go through graph_set_widget (Ctrl+Z). Flags need confirm:true; directly managed local Python installs get a proven relaunch with the flag appended and retained for later managed starts, while Desktop/remote/external launchers receive an actionable refusal without being stopped or edited. skip_proof:true skips the follow-up panel_run. A black or slower after-run reverts the widget.', {
|
|
18503
18550
|
action: z
|
|
18504
18551
|
.enum(["status", "assess", "apply"])
|
|
18505
18552
|
.describe('Which kitchen operation against the live graph.'),
|
|
@@ -18516,10 +18563,17 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
18516
18563
|
.optional()
|
|
18517
18564
|
.describe("If true, apply the widget/flag plan without queueing a proof panel_run."),
|
|
18518
18565
|
}, async (args, ctx) => {
|
|
18566
|
+
const target = capturePanelKitchenTarget(ctx);
|
|
18567
|
+
if (!target) {
|
|
18568
|
+
return fail("panel_kitchen refused: the process target could not be tied to this panel tab's exact ComfyUI target and generation. Keep one target selected, then retry.");
|
|
18569
|
+
}
|
|
18519
18570
|
const status = await gatherKitchenStatus();
|
|
18571
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18572
|
+
return fail("panel_kitchen refused: the ComfyUI target changed while status was being assessed. No recommendation was applied; retry after the target settles.");
|
|
18573
|
+
}
|
|
18520
18574
|
if (args.action === "status") {
|
|
18521
18575
|
const hint = kitchenProactiveHint(status, assessKitchen(status, {}));
|
|
18522
|
-
return ok(hint ? { status, hint } : { status });
|
|
18576
|
+
return ok(hint ? { status, hint, target_fence: target.fence } : { status, target_fence: target.fence });
|
|
18523
18577
|
}
|
|
18524
18578
|
const query = await ctx.call({
|
|
18525
18579
|
cmd: "graph_query",
|
|
@@ -18530,6 +18584,9 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
18530
18584
|
});
|
|
18531
18585
|
if (query.isError)
|
|
18532
18586
|
return query;
|
|
18587
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18588
|
+
return fail("panel_kitchen refused: the ComfyUI target changed while the live graph was being read. No recommendation was applied; retry after the target settles.");
|
|
18589
|
+
}
|
|
18533
18590
|
const graph = normalizeGraphQueryResult(query);
|
|
18534
18591
|
if (graph.truncated === true) {
|
|
18535
18592
|
const truncationCause = graph.truncated_by === "limit"
|
|
@@ -18542,12 +18599,16 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
18542
18599
|
`more specific workflow or retry after reducing the number of UNETLoaders.`);
|
|
18543
18600
|
}
|
|
18544
18601
|
const recs = await assessKitchenGraph(status, graph);
|
|
18602
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18603
|
+
return fail("panel_kitchen refused: the ComfyUI target changed during assessment. No recommendation was applied; retry after the target settles.");
|
|
18604
|
+
}
|
|
18545
18605
|
if (args.action === "assess") {
|
|
18546
18606
|
const hint = kitchenProactiveHint(status, recs);
|
|
18547
18607
|
return ok({
|
|
18548
18608
|
status,
|
|
18549
18609
|
loaders: extractLoaders(graph),
|
|
18550
18610
|
recommendations: recs,
|
|
18611
|
+
target_fence: target.fence,
|
|
18551
18612
|
...(hint ? { hint } : {}),
|
|
18552
18613
|
});
|
|
18553
18614
|
}
|
|
@@ -18561,28 +18622,123 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
18561
18622
|
if (!rec) {
|
|
18562
18623
|
return fail(`No recommendation "${args.recommendation_id}" on the live graph. Call panel_kitchen action:"assess" first. Known: ${recs.map((r) => r.id).join(", ") || "(none)"}`);
|
|
18563
18624
|
}
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
|
|
18571
|
-
|
|
18625
|
+
let result;
|
|
18626
|
+
try {
|
|
18627
|
+
result = await applyKitchenRecommendation({
|
|
18628
|
+
rec,
|
|
18629
|
+
confirm: args.confirm === true,
|
|
18630
|
+
applyFlag: rec.change.type === "flag"
|
|
18631
|
+
? async (flag) => {
|
|
18632
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18633
|
+
return {
|
|
18634
|
+
applied: false,
|
|
18635
|
+
note: `Refusing to apply ${flag}: the ComfyUI target changed after assessment. ` +
|
|
18636
|
+
"No launch argument was changed and no restart was attempted.",
|
|
18637
|
+
};
|
|
18638
|
+
}
|
|
18639
|
+
let restart;
|
|
18640
|
+
try {
|
|
18641
|
+
restart = await restartComfyUI({
|
|
18642
|
+
additionalFlags: [flag],
|
|
18643
|
+
targetFence: target.fence,
|
|
18644
|
+
});
|
|
18645
|
+
}
|
|
18646
|
+
catch (err) {
|
|
18647
|
+
return {
|
|
18648
|
+
applied: false,
|
|
18649
|
+
note: `Could not apply ${flag}: ${err instanceof Error ? err.message : String(err)} ` +
|
|
18650
|
+
"No launch argument was changed; verify the launcher and current ComfyUI argv before retrying.",
|
|
18651
|
+
};
|
|
18652
|
+
}
|
|
18653
|
+
const flagObserved = restart.serving_argv?.includes(flag) === true;
|
|
18654
|
+
const targetProven = restart.target_stable !== false &&
|
|
18655
|
+
targetFencesEqual(restart.target_fence, target.fence) &&
|
|
18656
|
+
panelKitchenTargetStillCurrent(ctx, target);
|
|
18657
|
+
const applied = restart.started === true &&
|
|
18658
|
+
restart.startup === "confirmed" &&
|
|
18659
|
+
restart.listener_ownership === "ours" &&
|
|
18660
|
+
flagObserved &&
|
|
18661
|
+
targetProven;
|
|
18662
|
+
return {
|
|
18663
|
+
applied,
|
|
18664
|
+
note: applied
|
|
18665
|
+
? `Applied ${flag} through the proven local relaunch and observed it in the serving ComfyUI argv. The augmented launch recipe is retained for later managed starts.`
|
|
18666
|
+
:
|
|
18667
|
+
`Did not confirm ${flag} in effect: ${restart.message} ` +
|
|
18668
|
+
"panel_kitchen reports applied:false because the relaunch, target fence, and new serving argv were not all proven.",
|
|
18669
|
+
restart,
|
|
18670
|
+
};
|
|
18572
18671
|
}
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
|
|
18672
|
+
: undefined,
|
|
18673
|
+
applyWidget: rec.change.type === "widget" || rec.change.type === "model_swap"
|
|
18674
|
+
? async (nodeId, widget, value) => {
|
|
18675
|
+
const write = await ctx.call({ cmd: "graph_set_widget", node_id: nodeId, widget, value }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS, undefined, () => {
|
|
18676
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18677
|
+
throw new Error("Refusing graph_set_widget: the ComfyUI target changed after the reachability wait. No widget mutation was dispatched.");
|
|
18678
|
+
}
|
|
18679
|
+
});
|
|
18680
|
+
if (write.isError) {
|
|
18681
|
+
throw new Error(write.content.map((c) => ("text" in c ? c.text : "")).join("\n"));
|
|
18682
|
+
}
|
|
18683
|
+
return parseToolResultJson(write);
|
|
18684
|
+
}
|
|
18685
|
+
: undefined,
|
|
18686
|
+
revertWidget: rec.change.type === "widget" || rec.change.type === "model_swap"
|
|
18687
|
+
? async (nodeId, widget, value) => {
|
|
18688
|
+
const revert = await ctx.call({ cmd: "graph_set_widget", node_id: nodeId, widget, value }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS, undefined, () => {
|
|
18689
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18690
|
+
throw new Error("Refusing graph_set_widget revert: the ComfyUI target changed after the reachability wait. No widget mutation was dispatched.");
|
|
18691
|
+
}
|
|
18692
|
+
});
|
|
18693
|
+
if (revert.isError) {
|
|
18694
|
+
throw new Error(revert.content.map((c) => ("text" in c ? c.text : "")).join("\n"));
|
|
18695
|
+
}
|
|
18696
|
+
}
|
|
18697
|
+
: undefined,
|
|
18698
|
+
});
|
|
18699
|
+
}
|
|
18700
|
+
catch (err) {
|
|
18701
|
+
result = {
|
|
18702
|
+
applied: false,
|
|
18703
|
+
recommendation: rec,
|
|
18704
|
+
proof: { status: "failed" },
|
|
18705
|
+
flag_note: `Could not apply the recommendation: ${err instanceof Error ? err.message : String(err)} ` +
|
|
18706
|
+
"No further widget mutation was dispatched and the launch outcome is not confirmed.",
|
|
18707
|
+
};
|
|
18708
|
+
}
|
|
18709
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18584
18710
|
return ok({
|
|
18585
18711
|
...result,
|
|
18712
|
+
applied: false,
|
|
18713
|
+
stale: true,
|
|
18714
|
+
target_stable: false,
|
|
18715
|
+
target_fence: target.fence,
|
|
18716
|
+
flag_note: (result.flag_note ? `${result.flag_note} ` : "") +
|
|
18717
|
+
"panel_kitchen reports applied:false because the ComfyUI target changed before the result was returned.",
|
|
18718
|
+
});
|
|
18719
|
+
}
|
|
18720
|
+
const fencedResult = { ...result, target_fence: target.fence };
|
|
18721
|
+
if (fencedResult.applied && args.skip_proof !== true && rec.change.type !== "flag") {
|
|
18722
|
+
const run = await ctx.call({ cmd: "graph_run" });
|
|
18723
|
+
if (!panelKitchenTargetStillCurrent(ctx, target)) {
|
|
18724
|
+
return ok({
|
|
18725
|
+
...fencedResult,
|
|
18726
|
+
applied: false,
|
|
18727
|
+
stale: true,
|
|
18728
|
+
target_stable: false,
|
|
18729
|
+
proof: {
|
|
18730
|
+
...fencedResult.proof,
|
|
18731
|
+
status: "stale",
|
|
18732
|
+
summary: (fencedResult.proof.summary ? `${fencedResult.proof.summary}; ` : "") +
|
|
18733
|
+
"graph_run was dispatched for the previously fenced target, but the target changed before its result was returned; no success is claimed for the current target.",
|
|
18734
|
+
},
|
|
18735
|
+
run: parseToolResultJson(run),
|
|
18736
|
+
flag_note: "panel_kitchen reports applied:false/stale because the ComfyUI target changed while the proof graph_run was in flight.",
|
|
18737
|
+
target_note: "panel_kitchen reports applied:false/stale because the ComfyUI target changed while the proof graph_run was in flight.",
|
|
18738
|
+
});
|
|
18739
|
+
}
|
|
18740
|
+
return ok({
|
|
18741
|
+
...fencedResult,
|
|
18586
18742
|
proof: {
|
|
18587
18743
|
...result.proof,
|
|
18588
18744
|
status: "queued",
|
|
@@ -18592,7 +18748,7 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
18592
18748
|
run: parseToolResultJson(run),
|
|
18593
18749
|
});
|
|
18594
18750
|
}
|
|
18595
|
-
return ok(
|
|
18751
|
+
return ok(fencedResult);
|
|
18596
18752
|
}),
|
|
18597
18753
|
def("panel_ui_render", "Render an INTERACTIVE UI CARD in the panel chat from an A2UI-subset JSON spec — choice buttons, forms (TextField/Select/Checkbox + a submit Button), node-wiring diagrams (comfy:graph), and bar/line charts (comfy:chart). Use a card whenever the user must pick between options, confirm a plan, fill in parameters, or would understand a wiring explanation better as a diagram. The card is non-blocking: this returns { card_id } immediately; when the user clicks a button (or submits a form) their choice arrives as a NORMAL chat message (the button's `reply` text; submit buttons append 'name: value' lines) — so after rendering a card that asks a question, END YOUR TURN and wait. Set surface:'wide' for diagram-heavy cards (the panel widens and restores automatically). Spec shape: { surface?, title?, root: '<id>', components: [ {id, type, ...} ] } with children referenced by id. Types: Text{text}, Heading{text,level?}, Button{label,reply?,submit?,style?:'primary'|'secondary'}, Row/Column/Card{children:[ids]}, Divider, Image{src:/view-URL,caption?}, TextField{label,name,value?,placeholder?}, Select{label,name,options:[{label,value?}],value?}, Checkbox{label,name,checked?}, 'comfy:graph'{nodes:[{id,label,color?}],edges:[{from,to,label?}],direction?:'lr'|'tb'}, 'comfy:chart'{kind:'bar'|'line',series:[{label,values:[num]}],x?:[labels]}. Caps: ≤64 components, ≤30 graph nodes, ≤8×256 chart points. On a validation error, FIX the spec and retry.", {
|
|
18598
18754
|
spec: z
|