comfyui-mcp 0.50.7 → 0.50.9
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 +227 -40
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/orchestrator/todo-state.js +61 -0
- package/dist/orchestrator/todo-state.js.map +1 -1
- package/dist/services/skill-generator.js +56 -2
- package/dist/services/skill-generator.js.map +1 -1
- package/package.json +1 -1
|
@@ -65,7 +65,7 @@ import { addUserMcpServer, readUserMcpServers, removeUserMcpServer, setUserMcpSe
|
|
|
65
65
|
import { setComfyuiSecret, setAgentSecret, isAllowedAgentSecretKey, receiptDisclosures, shadowedNote, storeDamageNote, } from "../services/panel-secrets.js";
|
|
66
66
|
import { flattenUiWorkflow } from "../services/flatten-workflow.js";
|
|
67
67
|
import { describeUnappliedFilters } from "./civitai-filter-guard.js";
|
|
68
|
-
import { recordTodo } from "./todo-state.js";
|
|
68
|
+
import { recordTodo, normalizeTodoItems, TODO_STATUS_INPUTS } from "./todo-state.js";
|
|
69
69
|
import { applyCapturedWidgetValues } from "../services/live-widget-overlay.js";
|
|
70
70
|
import { listWorkflowLibraryKeys, userdataFetch } from "../services/userdata-library.js";
|
|
71
71
|
import { getNsfwConsent, setNsfwConsent } from "../services/panel-settings.js";
|
|
@@ -621,6 +621,33 @@ function isTransientReconnectError(err) {
|
|
|
621
621
|
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
622
622
|
return /no connected tab|genuinely gone|is not open|Failed to fetch|Panel not reachable|ECONNRESET|socket hang up|premature close|other side closed|ECONNABORTED|EPIPE/i.test(msg);
|
|
623
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* #1027 — the panel's WORKFLOW-SWITCH critical section, which is retryable by
|
|
626
|
+
* construction and was not being retried.
|
|
627
|
+
*
|
|
628
|
+
* While a workflow switch/reload is in flight the panel refuses every graph and
|
|
629
|
+
* workflow executor rather than queueing it, because refusing cannot reorder or
|
|
630
|
+
* double-apply. Its own words:
|
|
631
|
+
*
|
|
632
|
+
* the panel is switching/refreshing "<key>" right now, so "<cmd>" was NOT
|
|
633
|
+
* applied — nothing changed. Retry in a moment.
|
|
634
|
+
*
|
|
635
|
+
* Two facts make this the safest retry in the file: the panel STATES nothing was
|
|
636
|
+
* applied, and the section lasts a fraction of a second. Yet none of that text
|
|
637
|
+
* matched the transient classifier, so a read issued right after
|
|
638
|
+
* panel_open_workflow surfaced the refusal to the agent instead of waiting the
|
|
639
|
+
* ~400ms the panel asked for. A reporter driving several tabs read-only hit it
|
|
640
|
+
* repeatedly, and read the resulting instance-mismatch errors as a wedge.
|
|
641
|
+
*
|
|
642
|
+
* TEXT-matched, deliberately, unlike #1001's typed marker: this error is minted
|
|
643
|
+
* in the browser and arrives over the wire, so there is no object identity to
|
|
644
|
+
* carry a symbol across. The phrasing is distinctive enough to key on, and the
|
|
645
|
+
* retry it enables is bounded to RETRY-SAFE commands either way.
|
|
646
|
+
*/
|
|
647
|
+
function isWorkflowSwitchGuardRefusal(err) {
|
|
648
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
649
|
+
return /panel is switching\/refreshing|panel is switching or reloading/i.test(msg);
|
|
650
|
+
}
|
|
624
651
|
let retrySettleMsOverride = null;
|
|
625
652
|
/** Short pause before the single post-drop retry, letting the replacement tab
|
|
626
653
|
* finish its reconnect hello so ensureReachable can resolve it. Test-overridable. */
|
|
@@ -3956,13 +3983,31 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets) {
|
|
|
3956
3983
|
// For idempotent commands, settle briefly, rebind onto the now-live tab, and
|
|
3957
3984
|
// retry ONE time before surfacing an error (#278/#310/#332/#481). Mutating
|
|
3958
3985
|
// edits are excluded from RETRY_SAFE_CMDS, so they never double-apply.
|
|
3959
|
-
|
|
3986
|
+
// #1027 — the workflow-switch critical section is retried on the same path.
|
|
3987
|
+
// The panel refuses during it, states that nothing was applied, and asks
|
|
3988
|
+
// for a retry in a moment; the section lasts a fraction of a second, which
|
|
3989
|
+
// is what retrySettleMs already waits. Still gated on RETRY-SAFE commands,
|
|
3990
|
+
// so a mutation is never re-issued on our own initiative.
|
|
3991
|
+
if (isRetrySafeCmd(cmd) && (isTransientReconnectError(err) || isWorkflowSwitchGuardRefusal(err))) {
|
|
3960
3992
|
try {
|
|
3961
3993
|
await sleep(retrySettleMs());
|
|
3962
3994
|
ensureReachable(); // rebinds a current-mode session onto the reconnected tab
|
|
3963
3995
|
return ok(await sendRouted(cmd, timeoutMs, observeRid));
|
|
3964
3996
|
}
|
|
3965
3997
|
catch (err2) {
|
|
3998
|
+
// #1027 — a switch STILL in progress is not a reconnect, and saying so
|
|
3999
|
+
// would be the #1001 mistake again: the tab is connected and healthy,
|
|
4000
|
+
// it is simply mid-switch. Name the actual state and the actual wait.
|
|
4001
|
+
if (isWorkflowSwitchGuardRefusal(err2)) {
|
|
4002
|
+
const name = typeof cmd.cmd === "string" ? cmd.cmd : "panel command";
|
|
4003
|
+
return fail(`${name} was NOT applied — nothing changed. The panel is still switching or ` +
|
|
4004
|
+
`reloading the workflow on the canvas, which it refuses commands during so a ` +
|
|
4005
|
+
`command cannot land on the wrong graph. The tab is connected and this is not a ` +
|
|
4006
|
+
`reconnect: it normally clears in well under a second, so simply retry. If a ` +
|
|
4007
|
+
`switch appears stuck, check the canvas — a load dialog or an unsaved-changes ` +
|
|
4008
|
+
`prompt can hold it open awaiting the user. ` +
|
|
4009
|
+
`(${err2 instanceof Error ? err2.message : String(err2)})`);
|
|
4010
|
+
}
|
|
3966
4011
|
// The retry also failed — surface an actionable reconnecting status rather
|
|
3967
4012
|
// than a bare transport error (#332), while still failing honestly.
|
|
3968
4013
|
if (isTransientReconnectError(err2)) {
|
|
@@ -4992,6 +5037,66 @@ function validatePanelEditNodeArgs(args) {
|
|
|
4992
5037
|
* forwarded to the wire UNTOUCHED (contrast workflow_uuid, which is bridge-owned
|
|
4993
5038
|
* and always overwritten). Optional everywhere; never required.
|
|
4994
5039
|
*/
|
|
5040
|
+
/**
|
|
5041
|
+
* #845 — a node id the tools THEMSELVES printed must be accepted back.
|
|
5042
|
+
*
|
|
5043
|
+
* Every panel tool took `z.number().int()` for a node id, while the graph
|
|
5044
|
+
* readers return ids as STRINGS (`"id": "42"`). So the obvious move — copy an id
|
|
5045
|
+
* out of panel_query_graph, paste it into panel_select_nodes — failed on the
|
|
5046
|
+
* first attempt, every time, with a raw zod `expected number, received string`.
|
|
5047
|
+
* The reporter hit it doing exactly that.
|
|
5048
|
+
*
|
|
5049
|
+
* Nothing about `"42"` is ambiguous. Accept both spellings and normalize to the
|
|
5050
|
+
* number the wire has always carried, so the round trip closes.
|
|
5051
|
+
*
|
|
5052
|
+
* DELIBERATELY STRICT about what counts as a node id: only an integer, or a
|
|
5053
|
+
* string that is exactly an integer. `"42px"`, `"4.5"`, `""` and `"5:12"` are
|
|
5054
|
+
* still rejected. The last one matters — a subgraph-qualified id is a real
|
|
5055
|
+
* shape in newer ComfyUI, and silently truncating it to `5` would target the
|
|
5056
|
+
* WRONG node rather than fail. If those need supporting, that is a separate,
|
|
5057
|
+
* deliberate change to the wire contract, not something to fall out of a coerce.
|
|
5058
|
+
*/
|
|
5059
|
+
const nodeId = () => z.union([z.number().int(), z.string().regex(/^-?\d+$/, "a node id must be an integer")]).transform((v) => (typeof v === "number" ? v : Number.parseInt(v, 10)));
|
|
5060
|
+
/**
|
|
5061
|
+
* #845 — which `panel_canvas` arguments the chosen action actually consumes.
|
|
5062
|
+
*
|
|
5063
|
+
* The tool accepted node_id/dx/dy/scale for every action and forwarded them all,
|
|
5064
|
+
* so an argument the action ignores vanished without a word. The reporter passed
|
|
5065
|
+
* `zoom: 0.55` alongside `center_on_node` and got `scale: 0.067` back — which
|
|
5066
|
+
* reads as "your zoom was applied and then overridden", when in truth it was
|
|
5067
|
+
* never applied at all. The panel's center_on_node case sets the offset and
|
|
5068
|
+
* touches the scale not at all.
|
|
5069
|
+
*
|
|
5070
|
+
* Naming what an action ignores is the whole fix. It is NOT an error — passing a
|
|
5071
|
+
* harmless extra argument should not fail a viewport move — but it must not be
|
|
5072
|
+
* silent either.
|
|
5073
|
+
*/
|
|
5074
|
+
const CANVAS_ACTION_ARGS = {
|
|
5075
|
+
fit: [], // computes its own framing from the graph bounds
|
|
5076
|
+
center_on_node: ["node_id"],
|
|
5077
|
+
pan: ["dx", "dy"],
|
|
5078
|
+
zoom: ["scale"],
|
|
5079
|
+
};
|
|
5080
|
+
/** Supplied-but-unused argument names for `action`, in a caller-facing spelling. */
|
|
5081
|
+
export function ignoredCanvasArgs(action, supplied) {
|
|
5082
|
+
const used = CANVAS_ACTION_ARGS[action];
|
|
5083
|
+
if (!used)
|
|
5084
|
+
return []; // unknown action — the enum rejects it; never guess here
|
|
5085
|
+
const label = { scale: "scale/zoom" };
|
|
5086
|
+
return ["node_id", "dx", "dy", "scale"]
|
|
5087
|
+
.filter((k) => supplied[k] !== undefined && !used.includes(k))
|
|
5088
|
+
.map((k) => label[k] ?? k);
|
|
5089
|
+
}
|
|
5090
|
+
/** Append a disclosure line to a successful text result, leaving errors alone. */
|
|
5091
|
+
function appendNote(res, note) {
|
|
5092
|
+
const first = res.content[0];
|
|
5093
|
+
if (!first || first.type !== "text")
|
|
5094
|
+
return res;
|
|
5095
|
+
return {
|
|
5096
|
+
...res,
|
|
5097
|
+
content: [{ ...first, text: `${first.text}\n\n${note}` }, ...res.content.slice(1)],
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
4995
5100
|
const RETRY_OF_ARG = {
|
|
4996
5101
|
retry_of: z
|
|
4997
5102
|
.string()
|
|
@@ -5252,7 +5357,7 @@ export function buildPanelToolDefs() {
|
|
|
5252
5357
|
},
|
|
5253
5358
|
])),
|
|
5254
5359
|
def("panel_audit_prompt_director", "Audit Prompt Director on the LIVE canvas without changing it. Correlates Prompt Director/Producer/Auto/Context/Reference/Critic widget values and wiring with detected model-loader filenames, every LoRA loader's actual model/CLIP strengths, and Prompt Director's latest sanitized runtime edit plan, resolved Model Explorer metadata, warnings, exact final prompt, and critic verdict. Returns observations plus proposed panel_set_widget changes with requires_confirmation=true. Call this when Prompt Director nodes are present, before saying the model/LoRA setup is correct, or when an edit prompt is ignored. READ-ONLY: present useful findings to the user and ask before applying any recommendation unless they already explicitly asked you to fix it.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_prompt_director_audit" })),
|
|
5255
|
-
def("panel_get_subgraph", "Read INSIDE a subgraph node on the user's open graph: ids, types, widget values, and connections of its inner nodes. Use after panel_graph_outline / panel_query_graph shows a node with is_subgraph=true. Read-only.", { node_id:
|
|
5360
|
+
def("panel_get_subgraph", "Read INSIDE a subgraph node on the user's open graph: ids, types, widget values, and connections of its inner nodes. Use after panel_graph_outline / panel_query_graph shows a node with is_subgraph=true. Read-only.", { node_id: nodeId().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => withTruncationHints(await ctx.call({ cmd: "graph_get_subgraph", node_id: args.node_id }), [
|
|
5256
5361
|
{
|
|
5257
5362
|
flag: "truncated",
|
|
5258
5363
|
key: "truncation_hint",
|
|
@@ -5353,7 +5458,7 @@ export function buildPanelToolDefs() {
|
|
|
5353
5458
|
// placeholder — that fetch can outlast the 6000 ms default on a large
|
|
5354
5459
|
// install. Give it the bounded refresh ack budget.
|
|
5355
5460
|
ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS)),
|
|
5356
|
-
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id:
|
|
5461
|
+
def("panel_remove_node", "Remove a node (and its connections) from the user's open graph by id. Undoable with Ctrl+Z.", { node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_node", node_id: args.node_id })),
|
|
5357
5462
|
def("panel_clear", "Remove EVERY node from the user's open graph — only for an explicit 'clear/reset the canvas'. Just CALL THIS DIRECTLY when they ask to clear: the tool itself pops a confirm card and only wipes on a yes (don't ask separately first). The wipe is a single Ctrl+Z undo. NEVER use this for a 'new workflow' — that's panel_new_workflow (a new tab, leaves this graph intact).", {}, async (_args, ctx) => {
|
|
5358
5463
|
const decision = await ctx.confirm("Clear the canvas? This removes every node from the open workflow. (One Ctrl+Z undoes it.)", "Clear canvas");
|
|
5359
5464
|
if (decision === "timeout") {
|
|
@@ -5573,11 +5678,11 @@ export function buildPanelToolDefs() {
|
|
|
5573
5678
|
}
|
|
5574
5679
|
}),
|
|
5575
5680
|
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index. If both slot args are omitted the panel picks the first type-compatible pairing. On failure the error lists every slot with its type and [connected] flag — re-check with panel_query_graph ({ids:[node_id], fields:'detail'}). Undoable.", {
|
|
5576
|
-
from_node_id:
|
|
5681
|
+
from_node_id: nodeId().describe("Source node id."),
|
|
5577
5682
|
from_output: slotRef
|
|
5578
5683
|
.optional()
|
|
5579
5684
|
.describe("Source output slot name or index; omit to auto-match by type (prefers an unconnected, exact-type input; `*` wildcards match last)."),
|
|
5580
|
-
to_node_id:
|
|
5685
|
+
to_node_id: nodeId().describe("Target node id."),
|
|
5581
5686
|
to_input: slotRef
|
|
5582
5687
|
.optional()
|
|
5583
5688
|
.describe("Target input slot name or index; omit to auto-match by type (prefers an unconnected, exact-type input; `*` wildcards match last)."),
|
|
@@ -5604,11 +5709,11 @@ export function buildPanelToolDefs() {
|
|
|
5604
5709
|
auto_match: args.auto_match,
|
|
5605
5710
|
})),
|
|
5606
5711
|
def("panel_disconnect", "Disconnect an input slot of a node in the user's open graph. Undoable with Ctrl+Z.", {
|
|
5607
|
-
node_id:
|
|
5712
|
+
node_id: nodeId().describe("Node id whose input to disconnect."),
|
|
5608
5713
|
input: slotRef.optional().describe("Input slot name or index (default 0)."),
|
|
5609
5714
|
}, async (args, ctx) => ctx.call({ cmd: "graph_disconnect", node_id: args.node_id, input: args.input })),
|
|
5610
5715
|
def("panel_set_widget", "Set a widget value on a node in the user's open graph (steps, cfg, seed, ckpt_name, text prompts, …). Returns the previous and new value. Undoable with Ctrl+Z. To CLEAR a text widget to an empty string, pass `clear: true` (some MCP clients drop an empty-string `value` from the serialized payload, so `value: \"\"` may not arrive — `clear: true` always works). For the LTXDirector timeline node (WhatDreamsCost CSGlide), set `timeline_data` with the FULL timeline JSON (segments + global_prompt) to drive its custom timeline UI — this re-syncs the editor and regenerates its derived `local_prompts`/`segment_lengths`/`guide_strength` widgets; setting those derived widgets directly is refused (they are silently reverted).", {
|
|
5611
|
-
node_id:
|
|
5716
|
+
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
5612
5717
|
widget: z.string().describe("Widget name (e.g. 'steps', 'cfg', 'text')."),
|
|
5613
5718
|
value: z
|
|
5614
5719
|
.union([z.string(), z.number(), z.boolean()])
|
|
@@ -5634,7 +5739,7 @@ export function buildPanelToolDefs() {
|
|
|
5634
5739
|
return ctx.call({ cmd: "graph_set_widget", node_id: args.node_id, widget: args.widget, value }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
5635
5740
|
}),
|
|
5636
5741
|
def("panel_set_property", "Set a node's LiteGraph PROPERTY (the right-click → Properties panel), NOT a widget — the counterpart to panel_set_widget, which only reaches `widgets`. Many custom nodes are configured entirely through node properties: e.g. the rgthree Fast Groups Bypasser's filters `matchTitle`, `matchColors`, `sort`, and `toggleRestriction` are node properties, and without `matchTitle` the node enumerates EVERY group in the workflow (a footgun). Sets node.properties[name] and, when the node defines an onPropertyChanged callback (rgthree and many LiteGraph nodes do), invokes it so the change takes effect LIVE (e.g. rgthree re-filters its group list). Returns the previous and new value. Undoable with Ctrl+Z.", {
|
|
5637
|
-
node_id:
|
|
5742
|
+
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
5638
5743
|
name: z
|
|
5639
5744
|
.string()
|
|
5640
5745
|
.describe("Property name from the node's right-click → Properties panel (e.g. 'matchTitle', 'matchColors', 'sort', 'toggleRestriction')."),
|
|
@@ -5643,8 +5748,8 @@ export function buildPanelToolDefs() {
|
|
|
5643
5748
|
.describe("New property value (string/number/boolean/null). For the rgthree Fast Groups Bypasser, matchTitle is a title substring/regex filter."),
|
|
5644
5749
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_node_property", node_id: args.node_id, name: args.name, value: args.value })),
|
|
5645
5750
|
def("panel_edit_node", "Atomically edit one node, or apply the same edit to several nodes. Pass exactly one of node_id or node_ids, plus at least one field. In one Ctrl+Z step you can move (pos), resize (size — including Note/MarkdownNote), retitle, recolor, change shape, collapse, pin, or set execution mode. Widget values, LiteGraph properties, links, and slot order stay on their dedicated tools. For a multi-node call, position/size/title/mode apply the same value to every target. Color fields accept #RGB, #RGBA, #RRGGBB, or #RRGGBBAA; null clears a color. Bypassing a subgraph retains panel_set_node_mode's unsafe-boundary guard; force:true is required to override it. Undoable with Ctrl+Z.", {
|
|
5646
|
-
node_id:
|
|
5647
|
-
node_ids: z.array(
|
|
5751
|
+
node_id: nodeId().optional().describe("One node id from panel_graph_outline / panel_query_graph. Provide this OR node_ids, not both."),
|
|
5752
|
+
node_ids: z.array(nodeId()).min(1).optional().describe("Several node ids that receive the same presentation edit. Provide this OR node_id, not both."),
|
|
5648
5753
|
pos: xy().optional().describe("New canvas [x, y]."),
|
|
5649
5754
|
size: nodeSize().optional().describe("New [width, height] in canvas px. Uses the node's setSize so DOM-widget nodes reflow and minimum sizes are honored."),
|
|
5650
5755
|
title: z.string().optional().describe("New header title."),
|
|
@@ -5680,8 +5785,8 @@ export function buildPanelToolDefs() {
|
|
|
5680
5785
|
// Keep legacy bridge commands behind compatibility tool names. graph_edit_node
|
|
5681
5786
|
// is newer than several installed panels, while current panels adapt these
|
|
5682
5787
|
// commands into the same atomic implementation.
|
|
5683
|
-
def("panel_move_node", "Compatibility wrapper for panel_edit_node(pos).", { node_id:
|
|
5684
|
-
def("panel_resize_node", "Compatibility wrapper for panel_edit_node(size).", { node_id:
|
|
5788
|
+
def("panel_move_node", "Compatibility wrapper for panel_edit_node(pos).", { node_id: nodeId(), pos: xy() }, async (args, ctx) => ctx.call({ cmd: "graph_move_node", node_id: args.node_id, pos: args.pos })),
|
|
5789
|
+
def("panel_resize_node", "Compatibility wrapper for panel_edit_node(size).", { node_id: nodeId(), size: nodeSize() }, async (args, ctx) => ctx.call({ cmd: "graph_resize_node", node_id: args.node_id, size: args.size })),
|
|
5685
5790
|
def("panel_auto_layout", "Automatically arrange the user's open graph (or a subset of nodes) into a clean left-to-right / top-to-bottom / grid layout based on the real link topology. Group boxes move with their members and are re-fit. Use dry_run:true to preview proposed positions without touching the canvas. Undoable (one Ctrl+Z).", {
|
|
5686
5791
|
node_ids: z
|
|
5687
5792
|
.array(z.number().int())
|
|
@@ -5712,18 +5817,45 @@ export function buildPanelToolDefs() {
|
|
|
5712
5817
|
}, 15000)),
|
|
5713
5818
|
def("panel_canvas", "MOVE the user's viewport: 'fit' frames the whole graph, 'center_on_node' jumps to a node (give node_id), 'pan' shifts by dx/dy, 'zoom' sets an absolute scale. It changes what they are looking AT and returns nothing about the graph — to find out what is ON the canvas, use panel_graph_outline. View-only.", {
|
|
5714
5819
|
action: z.enum(["fit", "center_on_node", "pan", "zoom"]),
|
|
5715
|
-
node_id:
|
|
5820
|
+
node_id: nodeId().optional().describe("Required for center_on_node."),
|
|
5716
5821
|
dx: z.number().optional().describe("Pan delta x."),
|
|
5717
5822
|
dy: z.number().optional().describe("Pan delta y."),
|
|
5718
5823
|
scale: z.number().optional().describe("Absolute zoom for 'zoom' (0.05–4, 1 = 100%)."),
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5824
|
+
// #845 — the tool used two names for one concept: `action:"zoom"` requires
|
|
5825
|
+
// `scale`, and a `zoom` argument was simply not in the schema, so passing
|
|
5826
|
+
// it was dropped without a word. Accept it as the alias it obviously is.
|
|
5827
|
+
zoom: z.number().optional().describe("Alias for `scale`."),
|
|
5828
|
+
}, async (args, ctx) => {
|
|
5829
|
+
const scale = args.scale ?? args.zoom;
|
|
5830
|
+
const res = await ctx.call({
|
|
5831
|
+
cmd: "graph_canvas",
|
|
5832
|
+
action: args.action,
|
|
5833
|
+
node_id: args.node_id,
|
|
5834
|
+
dx: args.dx,
|
|
5835
|
+
dy: args.dy,
|
|
5836
|
+
scale,
|
|
5837
|
+
});
|
|
5838
|
+
// #845 — an argument this action does not consume was SILENTLY dropped.
|
|
5839
|
+
// The reporter passed zoom:0.55 to center_on_node, got scale 0.067 back,
|
|
5840
|
+
// and had no way to tell the zoom had been ignored rather than applied
|
|
5841
|
+
// and overridden. Only `zoom` applies a scale — the panel's
|
|
5842
|
+
// center_on_node sets the offset alone — so say which arguments this
|
|
5843
|
+
// action actually used.
|
|
5844
|
+
const ignored = ignoredCanvasArgs(String(args.action), {
|
|
5845
|
+
node_id: args.node_id,
|
|
5846
|
+
dx: args.dx,
|
|
5847
|
+
dy: args.dy,
|
|
5848
|
+
scale,
|
|
5849
|
+
});
|
|
5850
|
+
if (!ignored.length || res.isError)
|
|
5851
|
+
return res;
|
|
5852
|
+
return appendNote(res, `Note: action:"${args.action}" does not use ${ignored.join(", ")} — ` +
|
|
5853
|
+
`${ignored.length === 1 ? "it was" : "they were"} ignored, not applied. ` +
|
|
5854
|
+
(ignored.includes("scale/zoom")
|
|
5855
|
+
? `To change the zoom, call panel_canvas again with action:"zoom" and scale. `
|
|
5856
|
+
: "") +
|
|
5857
|
+
`Any values in this result are the canvas's actual state.`);
|
|
5858
|
+
}),
|
|
5727
5859
|
def("panel_run", "Queue the workflow the user has OPEN — exactly like them pressing Queue Prompt (current widget values, the live graph they can see). On success it confirms the run was queued; if ComfyUI REFUSES the prompt (validation failure on either channel — per-node node_errors OR a top-level error like a missing node type) it returns a FAILURE with that rejection detail, never a false 'queued'. Pass to_node_id to RUN ONLY ONE BRANCH ('run to node'): ComfyUI renders just that output node plus everything upstream of it and SKIPS every other output branch — handy for previewing or debugging part of a big graph without rendering the whole thing. to_node_id MUST be an OUTPUT node (SaveImage, PreviewImage, SaveVideo, …) — pick the one at the END of the branch you want; nodes are tagged is_output:true in panel_query_graph's detail rows. The output node may be NESTED inside a subgraph — just pass its id (resolved in the scope you're currently viewing, then anywhere in the workflow); the tool builds the nested execution path for you. Omit it to run the whole graph. DUPLICATE FENCE (#862): if a render this session cannot account for is already in flight (after a reconnect this is usually YOUR earlier render still running — the queue record does not survive a restart), the run is REFUSED before anything is queued and the in-flight prompt is named; inspect queue (action:'list') first, or pass allow_duplicate:true only to deliberately stack behind it. Use this so the render runs on THEIR canvas and they see the result.", {
|
|
5728
5860
|
batch_count: z
|
|
5729
5861
|
.number()
|
|
@@ -6366,8 +6498,13 @@ export function buildPanelToolDefs() {
|
|
|
6366
6498
|
items: z
|
|
6367
6499
|
.array(z.object({
|
|
6368
6500
|
text: z.string().describe("Short step description (a few words)."),
|
|
6501
|
+
// #1018 — accept the synonyms agents actually produce (in_progress,
|
|
6502
|
+
// completed, …) and normalize them in the handler. The DESCRIPTION
|
|
6503
|
+
// still teaches only the canonical trio: one vocabulary to learn,
|
|
6504
|
+
// and no round trip lost to a rejected first call over a spelling
|
|
6505
|
+
// whose intent was never in doubt.
|
|
6369
6506
|
status: z
|
|
6370
|
-
.enum(
|
|
6507
|
+
.enum(TODO_STATUS_INPUTS)
|
|
6371
6508
|
.optional()
|
|
6372
6509
|
.describe("Step state (default 'pending'). Mark the one you're on 'active'."),
|
|
6373
6510
|
}))
|
|
@@ -6382,6 +6519,10 @@ export function buildPanelToolDefs() {
|
|
|
6382
6519
|
// bound tab is a headless (mobile/remote) client, resolve the live desktop
|
|
6383
6520
|
// canvas tab instead of dispatching at the headless client — which would
|
|
6384
6521
|
// reject with the misleading "mobile client has no open canvas".
|
|
6522
|
+
// #1018 — canonicalize ONCE, here, before anything reads the list. The
|
|
6523
|
+
// panel, recordTodo's snapshot (#977) and the completion-directive logic
|
|
6524
|
+
// all keep seeing only pending/active/done.
|
|
6525
|
+
const items = normalizeTodoItems(args.items);
|
|
6385
6526
|
const redirect = desktopCanvasRedirect(ctx, "panel_set_todo");
|
|
6386
6527
|
if (redirect?.error)
|
|
6387
6528
|
return fail(redirect.error);
|
|
@@ -6389,14 +6530,14 @@ export function buildPanelToolDefs() {
|
|
|
6389
6530
|
// #977 — the desktop redirect writes the checklist to ANOTHER tab, so
|
|
6390
6531
|
// record it against that one. Keying it on ctx.tabId would leave the
|
|
6391
6532
|
// tab that actually holds the plan looking planless.
|
|
6392
|
-
recordTodo(redirect.tabId,
|
|
6393
|
-
return dispatchToTab(ctx, redirect.tabId, { cmd: "set_todo", items
|
|
6533
|
+
recordTodo(redirect.tabId, items);
|
|
6534
|
+
return dispatchToTab(ctx, redirect.tabId, { cmd: "set_todo", items }, 15000, () => reResolveDesktopTab(ctx, "panel_set_todo"));
|
|
6394
6535
|
}
|
|
6395
6536
|
// #977 — retain what the agent DECLARED, so a later render completion can
|
|
6396
6537
|
// tell "you are mid-sweep" from "that was the last thing you were doing".
|
|
6397
6538
|
// The panel keeps the UI copy; this is the orchestrator's own record.
|
|
6398
|
-
recordTodo(ctx.tabId,
|
|
6399
|
-
return ctx.call({ cmd: "set_todo", items
|
|
6539
|
+
recordTodo(ctx.tabId, items);
|
|
6540
|
+
return ctx.call({ cmd: "set_todo", items }, 15000);
|
|
6400
6541
|
}),
|
|
6401
6542
|
def("panel_open_civitai", "Open the in-panel CivitAI browser for the user, pre-seeded with a search term and suggested filters, so they can VISUALLY browse and pick a model / LoRA / checkpoint / workflow / image. When the user asks about — or you're recommending — specific CivitAI models/LoRAs/checkpoints (e.g. 'what's a good relight LoRA?'), PREFER opening this docked browser and highlighting your picks over a text-only answer: it docks beside the chat (dock defaults true) so chat and results stay visible together, and it lets the user SEE the actual cards instead of reading a table. Typical show-don't-tell flow: panel_open_civitai (docked) → panel_civitai_search to refine → panel_civitai_results to READ the metadata + URLs → panel_civitai_highlight the one(s) you recommend, with a brief text summary of why. Set a helpful query + filters matched to their goal (including the browsing level). Their selection comes back to you as a normal chat message — UNLESS the panel is muted, in which case they download it directly themselves. Prefer this over guessing a specific model or asking them to paste a URL.", {
|
|
6402
6543
|
query: z
|
|
@@ -6923,7 +7064,53 @@ export function buildPanelToolDefs() {
|
|
|
6923
7064
|
note: hint + rebindNote + (fence?.note ?? ""),
|
|
6924
7065
|
});
|
|
6925
7066
|
}),
|
|
6926
|
-
def("panel_new_workflow", "Open a brand-new BLANK workflow in a NEW TAB. Use this whenever the user wants a 'new workflow' / 'fresh canvas' / 'start over for a new project'. This does NOT touch their current workflow — it opens a separate tab. NEVER use panel_clear for a new workflow (panel_clear wipes the CURRENT graph and is only for 'clear/reset this canvas').", {}, async (_args, ctx) =>
|
|
7067
|
+
def("panel_new_workflow", "Open a brand-new BLANK workflow in a NEW TAB. Use this whenever the user wants a 'new workflow' / 'fresh canvas' / 'start over for a new project'. This does NOT touch their current workflow — it opens a separate tab. NEVER use panel_clear for a new workflow (panel_clear wipes the CURRENT graph and is only for 'clear/reset this canvas').", {}, async (_args, ctx) => {
|
|
7068
|
+
const res = await ctx.call({ cmd: "workflow_new" }, 15000);
|
|
7069
|
+
if (res.isError)
|
|
7070
|
+
return res;
|
|
7071
|
+
// #932 (recurrence on 0.50.6) — a NEW canvas needs a NEW fence.
|
|
7072
|
+
//
|
|
7073
|
+
// workflow_new authoritatively re-points the active workflow, exactly as
|
|
7074
|
+
// workflow_open does — but only the open path re-derived the command
|
|
7075
|
+
// fence afterwards (openWorkflowWithVerify). So this session kept the
|
|
7076
|
+
// PREVIOUS workflow's instance stamp while the user was now looking at a
|
|
7077
|
+
// brand-new blank canvas, and every stamped command after it failed with
|
|
7078
|
+
// "workflow instance mismatch". The reporter created a workflow and could
|
|
7079
|
+
// not add a single node to it.
|
|
7080
|
+
//
|
|
7081
|
+
// Refresh from the panel's own live active record, the same way the open
|
|
7082
|
+
// path does. The panel mints the new canvas's identity EAGERLY at
|
|
7083
|
+
// creation ("so the key exists BEFORE the first edit"), so it is readable
|
|
7084
|
+
// by the time this runs — it is simply not carried on the workflow_new
|
|
7085
|
+
// reply, which returns key/routing_key but no workflow_uuid.
|
|
7086
|
+
//
|
|
7087
|
+
// NEVER fails the call on a rebind miss: the workflow WAS created, and
|
|
7088
|
+
// retracting that would be the worse lie. Disclose instead, so the agent
|
|
7089
|
+
// learns the graph tools are not yet usable here rather than discovering
|
|
7090
|
+
// it one confusing mismatch at a time.
|
|
7091
|
+
const fenceRebind = await rebindWorkflowFence(ctx);
|
|
7092
|
+
let canMutateNow;
|
|
7093
|
+
let refusalCause;
|
|
7094
|
+
try {
|
|
7095
|
+
if (ctx.tabGraphMutationCapability) {
|
|
7096
|
+
const cap = ctx.tabGraphMutationCapability();
|
|
7097
|
+
canMutateNow = cap.known ? cap.canMutate : undefined;
|
|
7098
|
+
if (cap.known && !cap.canMutate)
|
|
7099
|
+
refusalCause = cap.because;
|
|
7100
|
+
}
|
|
7101
|
+
else {
|
|
7102
|
+
canMutateNow = ctx.tabCanMutateGraph?.();
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
catch {
|
|
7106
|
+
canMutateNow = undefined; // a guard that can throw is not a guard
|
|
7107
|
+
refusalCause = undefined;
|
|
7108
|
+
}
|
|
7109
|
+
const fence = describeFenceRebind(fenceRebind, canMutateNow, refusalCause);
|
|
7110
|
+
if (!fence || fence.binding === "bound")
|
|
7111
|
+
return res;
|
|
7112
|
+
return appendNote(res, `The blank workflow WAS created.${fence.note}`);
|
|
7113
|
+
}),
|
|
6927
7114
|
def("panel_open_workflow", "Open / switch to a workflow by path or filename (from panel_list_workflows). Switches the active tab to it. If the workflow was ALREADY open and its .json changed on disk out-of-band, the result carries stale:true (or stale:\"unknown\" when staleness couldn't be verified) with a stale_hint — the canvas still shows the version this tab loaded (it is NOT auto-reloaded); call panel_load_workflow to load the on-disk version.", { path: z.string().describe("Workflow path, filename, or key from panel_list_workflows.") },
|
|
6928
7115
|
// Verify-after-timeout (#215/#319/#496): a backgrounded/frozen or already-open
|
|
6929
7116
|
// tab can be slow to ack workflow_open even though the switch succeeded. On an
|
|
@@ -6938,8 +7125,8 @@ export function buildPanelToolDefs() {
|
|
|
6938
7125
|
path: z.string().optional().describe("Which workflow to close; omit for the active one."),
|
|
6939
7126
|
force: z.boolean().optional().describe("Close even with unsaved changes (discards them). Default false."),
|
|
6940
7127
|
}, async (args, ctx) => ctx.call({ cmd: "workflow_close", path: args.path, force: args.force }, 15000)),
|
|
6941
|
-
def("panel_select_nodes", "Select nodes on the user's canvas by id (highlights them, sets the multi-selection). Useful before panel_create_subgraph.", { node_ids: z.array(
|
|
6942
|
-
def("panel_create_subgraph", "Group the given nodes into a SUBGRAPH (ComfyUI 'Convert to Subgraph') on the user's canvas — collapses them into one subgraph node. Returns the new subgraph node id. Undoable with Ctrl+Z. To wrap an existing GROUP, prefer panel_subgraph_group (you don't have to list the node_ids yourself).", { node_ids: z.array(
|
|
7128
|
+
def("panel_select_nodes", "Select nodes on the user's canvas by id (highlights them, sets the multi-selection). Useful before panel_create_subgraph.", { node_ids: z.array(nodeId()).describe("Node ids to select.") }, async (args, ctx) => ctx.call({ cmd: "graph_select_nodes", node_ids: args.node_ids })),
|
|
7129
|
+
def("panel_create_subgraph", "Group the given nodes into a SUBGRAPH (ComfyUI 'Convert to Subgraph') on the user's canvas — collapses them into one subgraph node. Returns the new subgraph node id. Undoable with Ctrl+Z. To wrap an existing GROUP, prefer panel_subgraph_group (you don't have to list the node_ids yourself).", { node_ids: z.array(nodeId()).describe("Node ids to group into a subgraph.") }, async (args, ctx) => ctx.call({ cmd: "graph_create_subgraph", node_ids: args.node_ids }, 15000)),
|
|
6943
7130
|
def("panel_subgraph_group", "Wrap an existing GROUP's nodes into ONE subgraph node in a single step — the clean way to refactor a big graph into readable, TOGGLEABLE units. Pass the group by `group` (its title, e.g. 'REPLACEMENT MODE', or its numeric id from panel_query_graph's groups[]). LiteGraph groups don't own nodes — membership is geometric — so this computes which nodes sit inside the group box, selects them, and collapses them via ComfyUI 'Convert to Subgraph', returning the new subgraph node id + the wrapped node ids. After this you can toggle that whole region as ONE unit: panel_set_node_mode(node_id, 'bypass'/'active') on the subgraph node, then panel_run — e.g. queue one run with the region ON and one with it OFF. Undoable with Ctrl+Z. (For an arbitrary set of nodes that isn't a group, use panel_create_subgraph with explicit node_ids.)", {
|
|
6944
7131
|
group: z
|
|
6945
7132
|
.union([z.string(), z.number()])
|
|
@@ -6959,7 +7146,7 @@ export function buildPanelToolDefs() {
|
|
|
6959
7146
|
.describe("Reconnect pasted nodes' inputs to existing nodes where they line up (default false)."),
|
|
6960
7147
|
}, async (args, ctx) => ctx.call({ cmd: "graph_paste_nodes", pos: args.pos, connect_inputs: args.connect_inputs }, 15000)),
|
|
6961
7148
|
def("panel_save_subgraph", "Save a SUBGRAPH node to the user's reusable blueprint LIBRARY (publish), so it can be dropped into any workflow later. Pass node_id to pick the subgraph node (else a single selected subgraph node is used) and name to title the blueprint (defaults to the node's title). Runs programmatically — NO save dialog pops. The blueprint becomes the addable type 'SubgraphBlueprint.<name>' (use panel_add_subgraph or panel_list_subgraphs). Returns {saved: {name, type}}.", {
|
|
6962
|
-
node_id:
|
|
7149
|
+
node_id: nodeId().optional().describe("Subgraph node id to publish (is_subgraph=true). Omit to use the selected subgraph node."),
|
|
6963
7150
|
name: z.string().optional().describe("Blueprint name. Defaults to the subgraph node's title."),
|
|
6964
7151
|
}, async (args, ctx) => ctx.call({ cmd: "graph_save_subgraph", node_id: args.node_id, name: args.name }, 20000)),
|
|
6965
7152
|
def("panel_list_subgraphs", "List the saved subgraph BLUEPRINTS in the user's library (from panel_save_subgraph, plus any global/bundled ones). Each entry has {name, type, display_name, description, is_global} — use name/type with panel_add_subgraph to drop it onto the canvas. Read-only.\n\nTOKEN-BOUNDED like the other reads (panel#690). The reply's count field is always the LIBRARY TOTAL, so compare it against the returned array. When entries are withheld the reply carries truncated:true, a returned field, and a note — an absent entry in a TRUNCATED list is NOT evidence the blueprint does not exist, so narrow with `filter` (or raise `limit`, up to 500) before concluding anything. When a filter is applied the reply's matched field reports how many the filter selected, distinct from the count field, so matched:0 against a non-zero count means the filter missed, not that the library is empty.", {
|
|
@@ -7017,14 +7204,14 @@ export function buildPanelToolDefs() {
|
|
|
7017
7204
|
bounds: args.bounds,
|
|
7018
7205
|
}, 15000)),
|
|
7019
7206
|
def("panel_remove_group", "Remove a group box from the user's open graph. The nodes inside the group are NOT deleted — only the box. Undoable.", { group_id: z.number().int().describe("Group id from panel_query_graph's groups[] / panel_create_group.") }, async (args, ctx) => ctx.call({ cmd: "graph_remove_group", group_id: args.group_id }, 15000)),
|
|
7020
|
-
def("panel_set_node_title", "Compatibility wrapper for panel_edit_node(title).", { node_id:
|
|
7021
|
-
def("panel_set_node_collapsed", "Compatibility wrapper for panel_edit_node(collapsed).", { node_id:
|
|
7207
|
+
def("panel_set_node_title", "Compatibility wrapper for panel_edit_node(title).", { node_id: nodeId(), title: z.string() }, async (args, ctx) => ctx.call({ cmd: "graph_set_title", node_id: args.node_id, title: args.title }, 15000)),
|
|
7208
|
+
def("panel_set_node_collapsed", "Compatibility wrapper for panel_edit_node(collapsed).", { node_id: nodeId(), collapsed: z.boolean().optional() }, async (args, ctx) => ctx.call({ cmd: "graph_set_node_collapsed", node_id: args.node_id, collapsed: args.collapsed ?? true })),
|
|
7022
7209
|
def("panel_set_node_mode", "Set a node's EXECUTION MODE on the user's open graph — active, bypass, or mute — and return { node_id, mode, previous_mode }. This is how you turn a node ON or OFF without deleting it. Modes:\n" +
|
|
7023
7210
|
"• 'active' — normal: the node executes.\n" +
|
|
7024
7211
|
"• 'bypass' — the node is SKIPPED and PASSES ITS INPUT THROUGH to its output (downstream still runs, just as if this node weren't there). Use to disable a single processing node (an upscaler, a LoRA, a detailer) while keeping the pipeline connected.\n" +
|
|
7025
7212
|
"• 'mute' — the node AND everything DOWNSTREAM of it do NOT execute (no pass-through). Use to fully switch off a branch/output.\n" +
|
|
7026
7213
|
"CRITICAL — modes silently change what a render produces, so they are a top cause of 'wrong output'. A BYPASSED node contributes nothing of its own and a MUTED node kills its branch. Use this tool to ENABLE the path you actually want and DISABLE the one you don't — e.g. to drive a workflow from its Ideogram/JSON prompt builder you must set the manual-prompt node to 'bypass' and the JSON-builder path to 'active' (or vice-versa); likewise to pick one branch of an rgthree 'Fast Groups Bypasser'/Muter or a prompt-source switch. ALWAYS read modes first (panel_graph_outline marks [bypass]/[mute]; panel_query_graph detail rows carry mode): if the intended path is bypassed/muted, fix it HERE before running, and never assume a switch/route is already active. UNSAFE-BYPASS GUARD: bypassing a SUBGRAPH node whose boundary inputs are ordered differently from its outputs is REJECTED — ComfyUI forwards each output from the input at the SAME index, so e.g. an IMAGE output backed by a BBOX_DETECTOR input would silently feed the wrong type downstream. Re-order the boundary inputs or add an explicit ImpactSwitch to choose the passthrough; pass force:true only if you truly intend the positional forward. Undoable with Ctrl+Z.", {
|
|
7027
|
-
node_id:
|
|
7214
|
+
node_id: nodeId().describe("Node id from panel_graph_outline / panel_query_graph."),
|
|
7028
7215
|
mode: z
|
|
7029
7216
|
.enum(["active", "bypass", "mute"])
|
|
7030
7217
|
.describe("'active' = runs normally; 'bypass' = skipped, passes input through (downstream still runs); 'mute' = node and everything downstream do not execute."),
|
|
@@ -7034,7 +7221,7 @@ export function buildPanelToolDefs() {
|
|
|
7034
7221
|
.describe("Override the unsafe-bypass guard on a subgraph node (proceed with a positional boundary forward even when input/output types don't line up by index). Omit for normal safe behaviour."),
|
|
7035
7222
|
}, async (args, ctx) => ctx.call({ cmd: "graph_set_node_mode", node_id: args.node_id, mode: args.mode, force: args.force })),
|
|
7036
7223
|
def("panel_set_node_color", "Legacy color compatibility wrapper. Unlike panel_edit_node, color and bgcolor accept any CSS color string; when preset is supplied it wins over explicit colors, preserving the historical bridge behavior.", {
|
|
7037
|
-
node_id:
|
|
7224
|
+
node_id: nodeId(),
|
|
7038
7225
|
preset: z.enum(["red", "brown", "green", "blue", "pale_blue", "cyan", "purple", "yellow", "black"]).nullable().optional(),
|
|
7039
7226
|
color: z.string().nullable().optional(),
|
|
7040
7227
|
bgcolor: z.string().nullable().optional(),
|
|
@@ -7071,19 +7258,19 @@ export function buildPanelToolDefs() {
|
|
|
7071
7258
|
return fail(err);
|
|
7072
7259
|
}
|
|
7073
7260
|
}),
|
|
7074
|
-
def("panel_enter_subgraph", "Navigate INTO a subgraph node so you can read and EDIT its inner nodes — after this, panel_query_graph / panel_graph_outline and all panel_* edit tools target the subgraph's inner graph (the user sees the canvas drill in). This is how you edit inside a subgraph (e.g. tweak a widget on an inner node). Call panel_exit_subgraph when done. Returns the new viewing scope.", { node_id:
|
|
7261
|
+
def("panel_enter_subgraph", "Navigate INTO a subgraph node so you can read and EDIT its inner nodes — after this, panel_query_graph / panel_graph_outline and all panel_* edit tools target the subgraph's inner graph (the user sees the canvas drill in). This is how you edit inside a subgraph (e.g. tweak a widget on an inner node). Call panel_exit_subgraph when done. Returns the new viewing scope.", { node_id: nodeId().describe("Subgraph node id (is_subgraph=true).") }, async (args, ctx) => ctx.call({ cmd: "graph_enter_subgraph", node_id: args.node_id }, 15000)),
|
|
7075
7262
|
def("panel_exit_subgraph", "Leave the current subgraph and return to the root graph (undo a panel_enter_subgraph). After this, panel_* tools target the root graph again.", {}, async (_args, ctx) => ctx.call({ cmd: "graph_exit_subgraph" }, 15000)),
|
|
7076
7263
|
def("panel_move_rail", "Reposition a subgraph's input or output RAIL (the boundary I/O node that the inner wires connect to). You MUST be INSIDE the subgraph first (panel_enter_subgraph). Read current rail positions from panel_query_graph's `rails` field (present when viewing a subgraph). Use this to place the input rail just left of the first node column and the output rail just right of the last one, so a tidy interior layout doesn't leave the rails stranded. rail is 'input' or 'output'.", {
|
|
7077
7264
|
rail: z.enum(["input", "output"]).describe("Which boundary rail to move."),
|
|
7078
7265
|
pos: xy().describe("New top-left [x, y] (two numbers)."),
|
|
7079
7266
|
}, async (args, ctx) => ctx.call({ cmd: "graph_move_rail", rail: args.rail, pos: args.pos })),
|
|
7080
7267
|
def("panel_promote_widget", "Expose (promote) an INNER subgraph widget on the PARENT subgraph node, so it can be set from outside without opening the subgraph — e.g. surface an inner KSampler's `seed`/`steps` on the subgraph node. You MUST be inside the subgraph first (call panel_enter_subgraph): `node_id` is an inner node (from panel_query_graph while inside) and `widget` is one of its widget names. Pass demote:true to un-promote. Undoable with Ctrl+Z.", {
|
|
7081
|
-
node_id:
|
|
7268
|
+
node_id: nodeId().describe("Inner node id (from panel_query_graph while inside the subgraph)."),
|
|
7082
7269
|
widget: z.string().describe("Name of the widget on that node to promote (e.g. 'seed', 'steps', 'text')."),
|
|
7083
7270
|
demote: z.boolean().optional().describe("Set true to UN-promote (remove the widget from the parent node)."),
|
|
7084
7271
|
}, async (args, ctx) => ctx.call({ cmd: "graph_promote_widget", node_id: args.node_id, widget: args.widget, demote: args.demote }, 15000)),
|
|
7085
7272
|
def("panel_expose_subgraph_output", "Wire an interior node's OUTPUT to the subgraph's OUTPUT RAIL — i.e. expose it as a SUBGRAPH OUTPUT on the boundary so the PARENT graph can connect to the subgraph node's new output slot. You MUST be INSIDE the subgraph first (panel_enter_subgraph). This is the correct way to \"wire an internal output to the subgraph's output rail\": do NOT panel_connect to a guessed rail node id — call this with the interior node + the output you want exposed. Read panel_query_graph's `rails` to see the resulting boundary slots. `from_output` is an output slot NAME ('IMAGE', 'LATENT') or numeric index. Optional `name` titles the new boundary output (defaults from the source slot). Undoable with Ctrl+Z.", {
|
|
7086
|
-
from_node_id:
|
|
7273
|
+
from_node_id: nodeId().describe("Interior (inner) node id whose output to expose (from panel_query_graph while inside the subgraph)."),
|
|
7087
7274
|
from_output: slotRef.describe("Output slot name (e.g. 'IMAGE', 'LATENT') or numeric index on that node."),
|
|
7088
7275
|
name: z.string().optional().describe("Optional name for the new subgraph output (boundary slot). Defaults from the source slot."),
|
|
7089
7276
|
}, async (args, ctx) => ctx.call({
|
|
@@ -7093,7 +7280,7 @@ export function buildPanelToolDefs() {
|
|
|
7093
7280
|
name: args.name,
|
|
7094
7281
|
}, 15000)),
|
|
7095
7282
|
def("panel_expose_subgraph_input", "Wire an interior node's INPUT to the subgraph's INPUT RAIL — i.e. expose it as a SUBGRAPH INPUT on the boundary so the PARENT graph can feed the subgraph node's new input slot. You MUST be INSIDE the subgraph first (panel_enter_subgraph). This is the correct way to wire an internal input to the subgraph's input rail: do NOT panel_connect to a guessed rail node id — call this with the interior node + the input you want exposed. Read panel_query_graph's `rails` to see the resulting boundary slots. `to_input` is an input slot NAME ('model', 'pixels') or numeric index. Optional `name` titles the new boundary input (defaults from the target slot). Undoable with Ctrl+Z.", {
|
|
7096
|
-
to_node_id:
|
|
7283
|
+
to_node_id: nodeId().describe("Interior (inner) node id whose input to expose (from panel_query_graph while inside the subgraph)."),
|
|
7097
7284
|
to_input: slotRef.describe("Input slot name (e.g. 'model', 'pixels') or numeric index on that node."),
|
|
7098
7285
|
name: z.string().optional().describe("Optional name for the new subgraph input (boundary slot). Defaults from the target slot."),
|
|
7099
7286
|
}, async (args, ctx) => ctx.call({
|
|
@@ -7102,7 +7289,7 @@ export function buildPanelToolDefs() {
|
|
|
7102
7289
|
to_input: args.to_input,
|
|
7103
7290
|
name: args.name,
|
|
7104
7291
|
}, 15000)),
|
|
7105
|
-
def("panel_unpack_subgraph", "EXPAND / DISSOLVE a subgraph node on the user's open graph — inline its interior nodes back into the PARENT graph, rewire all external links to those now-inlined nodes, and remove the subgraph wrapper. This is the frontend's \"Unpack Subgraph\" (litegraph LGraph.unpackSubgraph) and the exact INVERSE of panel_create_subgraph. Use it to flatten a stage that was over-nested, or to edit interior nodes directly at the parent level. The interior nodes reappear on the parent canvas with their connections preserved. Undoable with Ctrl+Z.", { node_id:
|
|
7292
|
+
def("panel_unpack_subgraph", "EXPAND / DISSOLVE a subgraph node on the user's open graph — inline its interior nodes back into the PARENT graph, rewire all external links to those now-inlined nodes, and remove the subgraph wrapper. This is the frontend's \"Unpack Subgraph\" (litegraph LGraph.unpackSubgraph) and the exact INVERSE of panel_create_subgraph. Use it to flatten a stage that was over-nested, or to edit interior nodes directly at the parent level. The interior nodes reappear on the parent canvas with their connections preserved. Undoable with Ctrl+Z.", { node_id: nodeId().describe("Subgraph node id to unpack/dissolve (is_subgraph=true, from panel_graph_outline / panel_query_graph).") }, async (args, ctx) => ctx.call({ cmd: "graph_unpack_subgraph", node_id: args.node_id }, 15000)),
|
|
7106
7293
|
def("panel_search_nodes", "Search installable custom-node packs via the user's BUILT-IN ComfyUI Manager (the same source the Manager UI uses). Returns matching packs {id, title, description}. Use the `id` with panel_install_node. Prefer this over the headless search_custom_nodes tool — it works against the user's actual (Desktop) Manager.", { query: z.string().describe("Search text, e.g. 'kjnodes', 'controlnet', 'ipadapter'."), limit: z.number().int().min(1).max(40).optional() }, async (args, ctx) => ctx.call({ cmd: "nodes_search", query: args.query, limit: args.limit }, 20000)),
|
|
7107
7294
|
def("panel_list_nodes", "List the custom-node packs currently installed in the user's ComfyUI (via the built-in Manager). Read-only.", {}, async (_args, ctx) => ctx.call({ cmd: "nodes_list" }, 20000)),
|
|
7108
7295
|
def("panel_install_node", "Install a custom-node pack into the user's ComfyUI via the BUILT-IN Manager (queues the install). Pass `id` (registry id like 'comfyui-kjnodes' or 'author/repo') from panel_search_nodes, or `repository` (git URL) for a nightly install. A search result whose `id` IS a git URL (legacy/repository-style entries) is auto-routed to a from-source 'nightly' install — 'latest' cannot resolve for those. A ComfyUI restart (panel_restart_comfyui) is usually required afterward to load the nodes — poll panel_node_queue_status first. Prefer this over the headless install_custom_node tool. " +
|