In one real task the AI built an inpaint branch with 9 add_node and 16 connect_nodes calls. add_nodes takes the whole group in ComfyUI's API form, the format models know best: - links inside the group, and to canvas nodes as "#12", with outputs by slot or name; - a node's wire_to feeds canvas nodes the way add_node does. All of it is checked before anything changes. The page lays the group out as one block in a free spot (canvas_build.placedBlock) and applies it in one transaction: one Ctrl+Z step, and a failure takes every new node out again (applyWires now takes several nodes). The answer maps the AI's keys to node ids and lists what is spliced and what is still unwired. A splice now uses only an input that nothing else in the plan feeds. A node whose latent comes from the group, or from wire_from, replaces the sampler's empty latent instead (replace:true) rather than being fed twice. It is in the daily tool set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
3.7 KiB
JavaScript
74 lines
3.7 KiB
JavaScript
/**
|
|
* The task's own canvas tab (api/ai_task_canvas.py), on the page: before an AI's canvas action
|
|
* (mcp_bridge.js), with the task set to "new", the task's tab is brought to the front the way a
|
|
* click on it does (ComfyUI's store alone would not load its graph); with no tab yet, the first
|
|
* change copies the open workflow into a new tab named after the task and says so in a toast,
|
|
* so the user's own tab is never changed. Reads and runs follow the task's tab when it is open
|
|
* and use the canvas as it is before it has one; new_workflow and open_workflow make the tab
|
|
* they open the task's. An existing task tab that cannot be opened stops the action.
|
|
*/
|
|
|
|
import { app } from '../../../scripts/app.js';
|
|
import { translate as t } from './locales.js';
|
|
import { showWorkbenchToast } from './ui_prompt_toast.js';
|
|
import { tabName } from './mcp_canvas_edit.js';
|
|
|
|
const CHANGES = new Set(['set_prompt', 'set_model', 'add_lora', 'place_combo', 'fix_models', 'add_node', 'add_nodes', 'connect_nodes', 'set_values', 'remove_nodes']);
|
|
const FOLLOWS = new Set(['describe_canvas', 'check_models', 'run_workflow', 'download_missing', 'fill_slots']);
|
|
const OPENS = new Set(['new_workflow', 'open_workflow']);
|
|
const SWITCH_WAIT_MS = 4000;
|
|
|
|
const store = () => app.extensionManager?.workflow || null;
|
|
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
const shown = path => store()?.activeWorkflow?.path === path && !store()?.activeWorkflow?.isLoading;
|
|
|
|
function frontTab(made = false) {
|
|
const workflow = store()?.activeWorkflow;
|
|
return workflow ? { path: workflow.path, name: String(workflow.filename || workflow.path).replace(/\.json$/i, ''), made } : null;
|
|
}
|
|
|
|
async function settled(path, limit = SWITCH_WAIT_MS) {
|
|
for (let waited = 0; waited < limit && !shown(path); waited += 50) await wait(50);
|
|
if (shown(path)) await new Promise(resolve => requestAnimationFrame(resolve));
|
|
return shown(path);
|
|
}
|
|
|
|
/** Brings open tab `path` to the front as a click on it does; false when it is not open. */
|
|
export async function showTab(path) {
|
|
const workflows = store()?.openWorkflows || [];
|
|
const index = workflows.findIndex(workflow => workflow.path === path);
|
|
if (index < 0) return false;
|
|
if (shown(path)) return true;
|
|
document.querySelectorAll('.workflow-tabs > button')[index]?.click();
|
|
if (await settled(path)) return true;
|
|
// Tabs not on screen (another layout): step through the open ones, as Ctrl+Tab does.
|
|
for (let step = 0; step < workflows.length && !shown(path); step++) {
|
|
await app.extensionManager?.command?.execute?.('Workspace.NextOpenedWorkflow');
|
|
await settled(path, 600);
|
|
}
|
|
return shown(path);
|
|
}
|
|
|
|
/** Runs page action `action` (`run()`) on the task's tab per `spec` ({mode, tab, title}); `opened(tab)`
|
|
* hears of a tab made or opened for the task ({path, name, made}). Returns what `run()` does. */
|
|
export async function onTaskTab(action, spec, run, opened) {
|
|
if (spec?.mode !== 'new' || !(CHANGES.has(action) || FOLLOWS.has(action) || OPENS.has(action))) return run();
|
|
if (OPENS.has(action)) {
|
|
const value = await run();
|
|
opened(frontTab());
|
|
return value;
|
|
}
|
|
const path = spec.tab?.path;
|
|
if (path) {
|
|
if (!await showTab(path)) throw new Error('The task canvas tab is closed or could not be activated. '
|
|
+ 'Reopen that workflow, or explicitly use open_workflow/new_workflow for this task before retrying. Nothing was run or changed.');
|
|
return run();
|
|
}
|
|
if (!CHANGES.has(action)) return run();
|
|
const name = tabName(spec.title || t('aiTaskTabDefault'));
|
|
await app.loadGraphData(app.graph.serialize(), true, true, name);
|
|
opened(frontTab(true));
|
|
showWorkbenchToast(t('aiTaskTabMade', { name }));
|
|
return run();
|
|
}
|