comfyui-mcp 0.52.38 → 0.52.40
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.
|
@@ -3070,6 +3070,137 @@ function isLoraManagerAutocompleteRefusal(res) {
|
|
|
3070
3070
|
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
3071
3071
|
/AUTOCOMPLETE_TEXT_/i.test(text));
|
|
3072
3072
|
}
|
|
3073
|
+
/**
|
|
3074
|
+
* #1944 — the panel's add-node socket proof read "which datatypes does some
|
|
3075
|
+
* installed node output?" off a single-class `/object_info/<class_type>`
|
|
3076
|
+
* payload (or a whole-schema widen that did not land). Custom link types a
|
|
3077
|
+
* SIBLING node produces — SeedVR2LoadDiTModel → SEEDVR2_DIT — then look
|
|
3078
|
+
* unproven, and the refusal claims "no installed node outputs X" while that
|
|
3079
|
+
* very node sits on the canvas. Retrying the add re-runs the same proof;
|
|
3080
|
+
* panel_refresh_nodes re-registers the class, which is what arms the
|
|
3081
|
+
* single-class path. The live graph is the authority the guard did not consult.
|
|
3082
|
+
*/
|
|
3083
|
+
function isMissingInstalledOutputsRefusal(res) {
|
|
3084
|
+
if (!res.isError)
|
|
3085
|
+
return false;
|
|
3086
|
+
const text = textOfToolResult(res);
|
|
3087
|
+
return (/had no widget after .+ waiting for node extensions to register/i.test(text) &&
|
|
3088
|
+
/no installed node outputs/i.test(text));
|
|
3089
|
+
}
|
|
3090
|
+
/** Types the panel named as unproven in a #1944-shaped refusal. */
|
|
3091
|
+
function parseUnprovenSocketTypes(text) {
|
|
3092
|
+
const types = [];
|
|
3093
|
+
const add = (raw) => {
|
|
3094
|
+
const v = raw.trim();
|
|
3095
|
+
if (v && !types.includes(v))
|
|
3096
|
+
types.push(v);
|
|
3097
|
+
};
|
|
3098
|
+
for (const re of [
|
|
3099
|
+
/no installed node outputs "([^"]+)"/gi,
|
|
3100
|
+
/declared type "([^"]+)": no installed node outputs/gi,
|
|
3101
|
+
]) {
|
|
3102
|
+
re.lastIndex = 0;
|
|
3103
|
+
let m;
|
|
3104
|
+
while ((m = re.exec(text)))
|
|
3105
|
+
add(m[1]);
|
|
3106
|
+
}
|
|
3107
|
+
return types;
|
|
3108
|
+
}
|
|
3109
|
+
function nodeOutputDeclaresType(node, socketType) {
|
|
3110
|
+
const outputs = node.outputs;
|
|
3111
|
+
if (Array.isArray(outputs)) {
|
|
3112
|
+
for (const out of outputs) {
|
|
3113
|
+
if (!out || typeof out !== "object" || Array.isArray(out))
|
|
3114
|
+
continue;
|
|
3115
|
+
const t = out.type;
|
|
3116
|
+
if (typeof t !== "string" || !t)
|
|
3117
|
+
continue;
|
|
3118
|
+
if (t === socketType)
|
|
3119
|
+
return true;
|
|
3120
|
+
if (t.split(",").map((part) => part.trim()).includes(socketType))
|
|
3121
|
+
return true;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
const matchedOn = node.matched_on;
|
|
3125
|
+
if (!Array.isArray(matchedOn))
|
|
3126
|
+
return false;
|
|
3127
|
+
const needle = `(${socketType.toLowerCase()})`;
|
|
3128
|
+
return matchedOn.some((entry) => typeof entry === "string" && entry.toLowerCase().includes(needle));
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Ask the live canvas whether each unproven type is actually produced there.
|
|
3132
|
+
* Returns null when a probe fails — an unreadable graph is not evidence the
|
|
3133
|
+
* producers are present, so the original refusal stays the last word.
|
|
3134
|
+
*/
|
|
3135
|
+
async function findLiveSocketProducers(ctx, types) {
|
|
3136
|
+
const found = new Map();
|
|
3137
|
+
for (const socketType of types) {
|
|
3138
|
+
const probe = await ctx.call({ cmd: "graph_find_nodes", output: socketType, limit: 5 }, 8000);
|
|
3139
|
+
if (probe.isError)
|
|
3140
|
+
return null;
|
|
3141
|
+
const payload = parseToolResultJson(probe);
|
|
3142
|
+
const matches = Array.isArray(payload?.matches) ? payload.matches : [];
|
|
3143
|
+
const producers = [];
|
|
3144
|
+
for (const row of matches) {
|
|
3145
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
3146
|
+
continue;
|
|
3147
|
+
const rec = row;
|
|
3148
|
+
if (!nodeOutputDeclaresType(rec, socketType))
|
|
3149
|
+
continue;
|
|
3150
|
+
const id = rec.id;
|
|
3151
|
+
const type = rec.type ?? rec.class_type;
|
|
3152
|
+
producers.push({
|
|
3153
|
+
id: id == null ? "?" : String(id),
|
|
3154
|
+
type: typeof type === "string" && type ? type : "unknown",
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
found.set(socketType, producers);
|
|
3158
|
+
}
|
|
3159
|
+
return found;
|
|
3160
|
+
}
|
|
3161
|
+
function liveSocketProducerNote(classType, found) {
|
|
3162
|
+
const target = typeof classType === "string" && classType ? classType : "this node";
|
|
3163
|
+
const lines = [...found.entries()].map(([socket, nodes]) => {
|
|
3164
|
+
const who = nodes
|
|
3165
|
+
.slice(0, 3)
|
|
3166
|
+
.map((n) => `${n.type} #${n.id}`)
|
|
3167
|
+
.join(", ");
|
|
3168
|
+
return ` - ${socket}: ${who}`;
|
|
3169
|
+
});
|
|
3170
|
+
return (`\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
3171
|
+
`The live canvas ALREADY has nodes that output every type the add-node guard ` +
|
|
3172
|
+
`claimed was missing:\n` +
|
|
3173
|
+
`${lines.join("\n")}\n` +
|
|
3174
|
+
`The guard consulted a single-class /object_info payload (or a whole-schema ` +
|
|
3175
|
+
`widen that did not land), not the live frontend graph, so it cannot see ` +
|
|
3176
|
+
`sibling producers added earlier in this session. ` +
|
|
3177
|
+
`Retrying panel_add_node will keep failing; panel_refresh_nodes will not ` +
|
|
3178
|
+
`clear it (it re-registers the class, which is what arms the single-class path). ` +
|
|
3179
|
+
`Workaround: copy an existing "${target}" from another open workflow and ` +
|
|
3180
|
+
`paste it here (panel_copy_nodes / panel_paste_nodes), or reload the ComfyUI ` +
|
|
3181
|
+
`browser tab so the page rebuilds its node registry. Do not retry this class_type ` +
|
|
3182
|
+
`until then.`);
|
|
3183
|
+
}
|
|
3184
|
+
async function withLiveSocketProducerNote(res, classType, ctx) {
|
|
3185
|
+
if (!isMissingInstalledOutputsRefusal(res))
|
|
3186
|
+
return res;
|
|
3187
|
+
// #1708 is a Vue-widget miss, not a sibling-producer miss. Querying the
|
|
3188
|
+
// canvas for AUTOCOMPLETE_TEXT_* would only delay the named remedy.
|
|
3189
|
+
if (isLoraManagerAutocompleteRefusal(res))
|
|
3190
|
+
return res;
|
|
3191
|
+
const missing = parseUnprovenSocketTypes(textOfToolResult(res));
|
|
3192
|
+
if (!missing.length)
|
|
3193
|
+
return res;
|
|
3194
|
+
const found = await findLiveSocketProducers(ctx, missing);
|
|
3195
|
+
if (!found)
|
|
3196
|
+
return res;
|
|
3197
|
+
if (!missing.every((t) => (found.get(t)?.length ?? 0) > 0))
|
|
3198
|
+
return res;
|
|
3199
|
+
return appendToolResultText(res, liveSocketProducerNote(classType, found));
|
|
3200
|
+
}
|
|
3201
|
+
async function annotateAddNodeRefusal(res, classType, ctx) {
|
|
3202
|
+
return withFrontendOnlyPanelSkewNote(withLoraManagerAutocompleteNote(await withLiveSocketProducerNote(res, classType, ctx)), classType, ctx);
|
|
3203
|
+
}
|
|
3073
3204
|
const LORA_MANAGER_AUTOCOMPLETE_NOTE = `\n\nThis is NOT a missing pack and NOT a widget that is still loading. ` +
|
|
3074
3205
|
`ComfyUI-LoRA-Manager registers AUTOCOMPLETE_TEXT_* via getCustomWidgets() into ` +
|
|
3075
3206
|
`the frontend widget store; the add-node guard only looks at app.widgets (core types), ` +
|
|
@@ -3165,6 +3296,35 @@ function appendToolResultText(res, extra) {
|
|
|
3165
3296
|
content[idx] = { type: "text", text: `${block.text}${extra}` };
|
|
3166
3297
|
return { ...res, content };
|
|
3167
3298
|
}
|
|
3299
|
+
/**
|
|
3300
|
+
* #1873 — Save-As onto an EXISTING name 409s on purpose. ComfyUI's userdata POST
|
|
3301
|
+
* returns 409 when overwrite=false; the frontend's saveWorkflowAs then prompts
|
|
3302
|
+
* and, on confirm, DELETES the occupant. The panel refuses that delete path and
|
|
3303
|
+
* surfaces "choose a different name", which is what forced a third workflow file.
|
|
3304
|
+
*
|
|
3305
|
+
* Matched on the panel's relocating-save collision (`conflictError` in
|
|
3306
|
+
* workflow-save.js), not a generic "409 Conflict" — an in-place save can 409
|
|
3307
|
+
* over its OWN file (#442) and that remedy is not this one.
|
|
3308
|
+
*/
|
|
3309
|
+
function isSaveAsNameConflict(res) {
|
|
3310
|
+
if (!res.isError)
|
|
3311
|
+
return false;
|
|
3312
|
+
const text = textOfToolResult(res);
|
|
3313
|
+
return /already exists \(409 Conflict\)/i.test(text) && /choose a different name/i.test(text);
|
|
3314
|
+
}
|
|
3315
|
+
const SAVE_AS_NAME_CONFLICT_NOTE = `\n\nDo NOT pick a third name. Save-As never overwrites an existing file — that 409 is ` +
|
|
3316
|
+
`the copy contract, not a missing overwrite switch. To land the edited copy under the ` +
|
|
3317
|
+
`intended original name while keeping a backup of the pre-edit original:\n` +
|
|
3318
|
+
`1. panel_rename_workflow({path: "<original>", name: "<original>_pre_edit"}) — path can ` +
|
|
3319
|
+
`target a CLOSED listed workflow, so this does not disturb the active tab.\n` +
|
|
3320
|
+
`2. panel_rename_workflow({name: "<original>"}) — no path, so this renames the ACTIVE ` +
|
|
3321
|
+
`copy onto the freed name; the tab follows the file.\n` +
|
|
3322
|
+
`3. panel_save_workflow() — in-place save from then on.`;
|
|
3323
|
+
function withSaveAsNameConflictNote(res) {
|
|
3324
|
+
if (!isSaveAsNameConflict(res))
|
|
3325
|
+
return res;
|
|
3326
|
+
return appendToolResultText(res, SAVE_AS_NAME_CONFLICT_NOTE);
|
|
3327
|
+
}
|
|
3168
3328
|
// ---- #1695: bound the panel_set_widget previous/new echo --------------------
|
|
3169
3329
|
//
|
|
3170
3330
|
// Code-mode clients (Codex `functions.exec`) batch several panel_set_widget
|
|
@@ -10205,7 +10365,7 @@ export function buildPanelToolDefs() {
|
|
|
10205
10365
|
},
|
|
10206
10366
|
},
|
|
10207
10367
|
])),
|
|
10208
|
-
def("panel_add_node", "Add a node to the user's OPEN ComfyUI graph by class_type (e.g. 'KSampler', 'CheckpointLoaderSimple'). The user sees it appear live; Ctrl+Z undoes it. Returns the created node's id, slots, and default widget values. Frontend-only virtual types are addable too: 'Note' and 'MarkdownNote' — the supported way to ANNOTATE a workflow with on-canvas instructions (add the node, then put the text in its 'text' widget via panel_set_widget) — plus 'Reroute' and 'PrimitiveNode'. These are LiteGraph-native and never appear in the backend node registry, so they legitimately bypass the backend class_type check. ComfyUI-LoRA-Manager's 'Lora Loader (LoraManager)' (and other AUTOCOMPLETE_TEXT_* nodes) cannot be added: the pack is healthy, but the add-node guard cannot see its Vue autocomplete widget. Use 'LoRA Text Loader (LoraManager)' — same outputs, lora_syntax is a STRING — or core LoraLoader; reload/retry will not clear it. ADD NODES ONE AT A TIME, not as a parallel batch: each add carries a fresh /object_info payload and those register SERIALLY, so N concurrent adds become N sequential refresh cycles. On a large install that outruns the 30s per-command deadline and the later adds time out WHILE STILL QUEUED — they then apply when their turn arrives, leaving nodes you were told had failed (panel#767). Sequential adds each get the refresh to themselves and stay well inside the deadline.", {
|
|
10368
|
+
def("panel_add_node", "Add a node to the user's OPEN ComfyUI graph by class_type (e.g. 'KSampler', 'CheckpointLoaderSimple'). The user sees it appear live; Ctrl+Z undoes it. Returns the created node's id, slots, and default widget values. Frontend-only virtual types are addable too: 'Note' and 'MarkdownNote' — the supported way to ANNOTATE a workflow with on-canvas instructions (add the node, then put the text in its 'text' widget via panel_set_widget) — plus 'Reroute' and 'PrimitiveNode'. These are LiteGraph-native and never appear in the backend node registry, so they legitimately bypass the backend class_type check. ComfyUI-LoRA-Manager's 'Lora Loader (LoraManager)' (and other AUTOCOMPLETE_TEXT_* nodes) cannot be added: the pack is healthy, but the add-node guard cannot see its Vue autocomplete widget. Use 'LoRA Text Loader (LoraManager)' — same outputs, lora_syntax is a STRING — or core LoraLoader; reload/retry will not clear it. A 'no installed node outputs' refusal for a custom link type (SEEDVR2_DIT, SEEDVR2_VAE, …) after sibling producer nodes were just added is the guard looking at a single-class /object_info, not the live graph; retry/refresh will not clear it — copy the node from another workflow (panel_copy_nodes / panel_paste_nodes) or reload the tab. ADD NODES ONE AT A TIME, not as a parallel batch: each add carries a fresh /object_info payload and those register SERIALLY, so N concurrent adds become N sequential refresh cycles. On a large install that outruns the 30s per-command deadline and the later adds time out WHILE STILL QUEUED — they then apply when their turn arrives, leaving nodes you were told had failed (panel#767). Sequential adds each get the refresh to themselves and stay well inside the deadline.", {
|
|
10209
10369
|
class_type: z.string().describe("Exact ComfyUI node class_type to create."),
|
|
10210
10370
|
pos: xy()
|
|
10211
10371
|
.optional()
|
|
@@ -10219,7 +10379,7 @@ export function buildPanelToolDefs() {
|
|
|
10219
10379
|
const add = () => ctx.call({ cmd: "graph_add_node", class_type: args.class_type, pos: args.pos, title: args.title }, OBJECT_INFO_REFRESH_ACK_TIMEOUT_MS);
|
|
10220
10380
|
const first = await add();
|
|
10221
10381
|
if (!isStaleNodeSchemaRefusal(first)) {
|
|
10222
|
-
return
|
|
10382
|
+
return annotateAddNodeRefusal(first, args.class_type, ctx);
|
|
10223
10383
|
}
|
|
10224
10384
|
// #1329 — DO THE REFRESH THE REFUSAL ASKS FOR, instead of billing the caller.
|
|
10225
10385
|
//
|
|
@@ -10254,9 +10414,9 @@ export function buildPanelToolDefs() {
|
|
|
10254
10414
|
// the refresh failure alongside so the caller knows the automatic attempt
|
|
10255
10415
|
// happened and why it did not help. No retry here, because the schema really is
|
|
10256
10416
|
// unchanged and a second add really would refuse again.
|
|
10257
|
-
return
|
|
10417
|
+
return annotateAddNodeRefusal(appendToolResultText(first, `\n\n(Tried to clear this automatically: panel_refresh_nodes was dispatched and FAILED, ` +
|
|
10258
10418
|
`so the schema is unchanged and retrying the add will refuse again. ` +
|
|
10259
|
-
`${textOfToolResult(refreshed)})`)
|
|
10419
|
+
`${textOfToolResult(refreshed)})`), args.class_type, ctx);
|
|
10260
10420
|
}
|
|
10261
10421
|
// panel#1518 — TAKE THE RETRY, instead of telling the caller to take it.
|
|
10262
10422
|
//
|
|
@@ -10281,7 +10441,7 @@ export function buildPanelToolDefs() {
|
|
|
10281
10441
|
// refresh left behind.
|
|
10282
10442
|
const second = await add();
|
|
10283
10443
|
if (!isStaleNodeSchemaRefusal(second)) {
|
|
10284
|
-
return
|
|
10444
|
+
return annotateAddNodeRefusal(
|
|
10285
10445
|
// An error from the RETRY, on the path where the caller never asked for a
|
|
10286
10446
|
// retry, must say which attempt it came from — otherwise a bare
|
|
10287
10447
|
// `graph_add_node` timeout reads as the first add's outcome and leaves the
|
|
@@ -10292,12 +10452,12 @@ export function buildPanelToolDefs() {
|
|
|
10292
10452
|
`node schema and created NOTHING, then panel_refresh_nodes was dispatched and did ` +
|
|
10293
10453
|
`NOT answer within its window. The error above is from the second attempt, so at ` +
|
|
10294
10454
|
`most ONE add is unaccounted for here.)`)
|
|
10295
|
-
: second
|
|
10455
|
+
: second, args.class_type, ctx);
|
|
10296
10456
|
}
|
|
10297
10457
|
// Still stale after the refresh: report THAT, because it means the remedy the
|
|
10298
10458
|
// refusal prescribes does not fix this instance and a caller following it by hand
|
|
10299
10459
|
// would loop.
|
|
10300
|
-
return
|
|
10460
|
+
return annotateAddNodeRefusal(appendToolResultText(second, refreshOutranItsAck
|
|
10301
10461
|
? // The refresh was never observed to finish, so "repeating it will not clear
|
|
10302
10462
|
// it" and "reload the tab" would both overclaim. What IS known: it was not
|
|
10303
10463
|
// cancelled, it is still registering the definitions this add needs, and the
|
|
@@ -10313,7 +10473,7 @@ export function buildPanelToolDefs() {
|
|
|
10313
10473
|
`the tab being frozen. ${textOfToolResult(refreshed)})`
|
|
10314
10474
|
: `\n\n(This was already retried ONCE automatically: panel_refresh_nodes reported success ` +
|
|
10315
10475
|
`and the add still refuses, so repeating panel_refresh_nodes will not clear it. ` +
|
|
10316
|
-
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`)
|
|
10476
|
+
`Reload the ComfyUI browser tab, which rebuilds the page's node registry from scratch.)`), args.class_type, ctx);
|
|
10317
10477
|
}),
|
|
10318
10478
|
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 })),
|
|
10319
10479
|
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) => {
|
|
@@ -12210,7 +12370,7 @@ export function buildPanelToolDefs() {
|
|
|
12210
12370
|
multi_select: args.multi_select,
|
|
12211
12371
|
});
|
|
12212
12372
|
}),
|
|
12213
|
-
def("panel_save_workflow", "Save the user's open workflow PROGRAMMATICALLY — no Save/Rename dialog ever pops. With no `name`: saves in place (or auto-names + persists a never-saved workflow). With `name`: if the workflow is ALREADY saved under a different name this is a SAVE-AS — it writes a NEW file and leaves the original untouched on disk (it NEVER renames/moves/destroys the original); for a never-saved workflow it is simply the first save. The result reports what happened: `saved_as`+`copied_from`+`original_on_disk` (a disk-verified check that the original file still exists) for a Save-As copy, or `first_save` for a brand-new workflow. Use this freely (e.g. after building a graph) — it won't interrupt the user.", { name: z.string().optional().describe("Name for the workflow (no .json needed). If the workflow is already saved under a different name, this writes a NEW file (Save-As COPY) and leaves the original in place — it never renames/moves/destroys it. Omit to save in place / auto-name an unsaved workflow.") }, async (args, ctx) => {
|
|
12373
|
+
def("panel_save_workflow", "Save the user's open workflow PROGRAMMATICALLY — no Save/Rename dialog ever pops. With no `name`: saves in place (or auto-names + persists a never-saved workflow). With `name`: if the workflow is ALREADY saved under a different name this is a SAVE-AS — it writes a NEW file and leaves the original untouched on disk (it NEVER renames/moves/destroys the original); for a never-saved workflow it is simply the first save. The result reports what happened: `saved_as`+`copied_from`+`original_on_disk` (a disk-verified check that the original file still exists) for a Save-As copy, or `first_save` for a brand-new workflow. A Save-As onto an EXISTING name is a 409 Conflict (the copy never overwrites) — do NOT pick a third name; panel_rename_workflow can target a closed listed original to free the name, then rename the active copy onto it, then panel_save_workflow() in place. Use this freely (e.g. after building a graph) — it won't interrupt the user.", { name: z.string().optional().describe("Name for the workflow (no .json needed). If the workflow is already saved under a different name, this writes a NEW file (Save-As COPY) and leaves the original in place — it never renames/moves/destroys it. An existing target name 409s; do not invent a third name — use panel_rename_workflow to free the original then rename the active copy onto it. Omit to save in place / auto-name an unsaved workflow.") }, async (args, ctx) => {
|
|
12214
12374
|
// #402: await a stable tab binding before dispatching the (mutating) save, so
|
|
12215
12375
|
// a save issued in the post-restart "Connected: none" window reaches a live
|
|
12216
12376
|
// tab instead of failing with a bare "Failed to fetch"/OUTCOME UNKNOWN. Pre-
|
|
@@ -12237,8 +12397,11 @@ export function buildPanelToolDefs() {
|
|
|
12237
12397
|
}
|
|
12238
12398
|
}
|
|
12239
12399
|
}
|
|
12240
|
-
if (res.isError)
|
|
12241
|
-
|
|
12400
|
+
if (res.isError) {
|
|
12401
|
+
// #1873 — named Save-As 409s by contract; the panel's "choose a different
|
|
12402
|
+
// name" is what forced a third file. Keep the 409 and name the rename path.
|
|
12403
|
+
return args.name ? withSaveAsNameConflictNote(res) : res;
|
|
12404
|
+
}
|
|
12242
12405
|
// #1045 — a SAVE-AS replaces the active workflow instance: the canvas is
|
|
12243
12406
|
// now the NEW file, with its own identity, while this session's command
|
|
12244
12407
|
// fence still names the pre-save one. Every graph call afterwards fails
|
|
@@ -13000,9 +13163,9 @@ CHECKED FOR YOU: the graph read this message prescribes was just run, and it ` +
|
|
|
13000
13163
|
// ack-timeout, confirm via the authoritative workflow_list active identity
|
|
13001
13164
|
// before reporting failure — see openWorkflowWithVerify.
|
|
13002
13165
|
async (args, ctx) => openWorkflowWithVerify(args.path, ctx)),
|
|
13003
|
-
def("panel_rename_workflow", "Rename a workflow (the active one, or the one matching `path`).", {
|
|
13166
|
+
def("panel_rename_workflow", "Rename a workflow (the active one, or the one matching `path`). `path` can name a CLOSED listed workflow — renaming it does not disturb the active tab. After a Save-As copy, this is how to land the edited graph under the original name without a third file: rename the closed original aside (`path` + new name), then rename the active copy onto the freed name (no `path`), then panel_save_workflow().", {
|
|
13004
13167
|
name: z.string().describe("New name (no .json needed)."),
|
|
13005
|
-
path: z.string().optional().describe("Which workflow to rename; omit for the active one."),
|
|
13168
|
+
path: z.string().optional().describe("Which workflow to rename; omit for the active one. Can name a CLOSED listed workflow (does not disturb the active tab)."),
|
|
13006
13169
|
}, async (args, ctx) => ctx.call({ cmd: "workflow_rename", name: args.name, path: args.path }, 15000)),
|
|
13007
13170
|
def("panel_close_workflow", "Close a workflow tab (the active one, or the one matching `path`). Refuses if it has unsaved changes unless force:true — save first to avoid losing the user's work.", {
|
|
13008
13171
|
path: z.string().optional().describe("Which workflow to close; omit for the active one."),
|