refactor(recipes): keep append as canvas action

This commit is contained in:
DemonGatanjieu
2026-08-04 00:35:13 +08:00
parent 9daa71ba2a
commit 014c1fe4cf
6 changed files with 27 additions and 109 deletions
+18 -1
View File
@@ -558,4 +558,21 @@
- `node --check web/modules/ui_recipe_detail.js`
- `node --check web/modules/ui_recipes.js`
- `node tests/recipe_parser_roundtrip.mjs`
- `python_embeded/python.exe tests/test_recipe_roundtrip.py`
- `python_embeded/python.exe tests/test_recipe_roundtrip.py`
## [Snapshot] 2026-08-04 - Remove ambiguous recipe Open action
**Implemented**
- Removed “Open to Canvas� buttons from recipe cards and recipe detail views.
- Kept “Append to Canvas� as the only recipe composition action, so saved recipes never replace the user's current canvas or depend on ComfyUI workflow-tab semantics.
- Kept the separate structural-edit action as an explicitly confirmed new-canvas editing flow.
- Removed obsolete open-action locale strings and the unused active-workflow loader.
**Validation**
- `node --check web/modules/ui_recipe_detail.js`
- `node --check web/modules/ui_recipes.js`
- `node --check web/modules/locales.js`
- `node tests/recipe_parser_roundtrip.mjs`
- `python_embeded/python.exe tests/test_recipe_roundtrip.py`
- `python_embeded/python.exe -m py_compile api/recipes.py api/metadata.py api/models.py api/recipe_packages.py`
+4 -1
View File
@@ -442,4 +442,7 @@ Browser `title` hints are delayed, low-contrast, difficult to style, and poor fo
**The Solution**: Batch-resolve unresolved references by hash, exact size, and constrained model category while ignoring saved filenames and paths as identity evidence. Show the local candidate first, then require an explicit Apply action that updates the matching widget through the full-recipe update path and preserves history.
## 66. ComfyUI workflow-tab loads need an explicit active workflow
In current ComfyUI frontend builds, `app.loadGraphData(data)` without its fourth argument creates or opens a temporary workflow tab. A plugin that intends to replace the current canvas must pass the active `ComfyWorkflow` object. For legacy-compatible extensions, discover the workflow store from the mounted Vue app's provided Pinia instance when available, then retain the old call as a fallback. Also make plugin-shell cleanup explicit after a successful canvas transition; hiding only the inner panel can leave an empty modal visible when the frontend changes workflow tabs.
In current ComfyUI frontend builds, `app.loadGraphData(data)` without its fourth argument creates or opens a temporary workflow tab. A plugin that intends to replace the current canvas must pass the active `ComfyWorkflow` object. For legacy-compatible extensions, discover the workflow store from the mounted Vue app's provided Pinia instance when available, then retain the old call as a fallback. Also make plugin-shell cleanup explicit after a successful canvas transition; hiding only the inner panel can leave an empty modal visible when the frontend changes workflow tabs.
## 67. Prefer one unambiguous recipe canvas action
When ComfyUI versions give “Open” different meanings—replace the current graph in one version and open a new workflow tab in another—do not preserve a confusing button merely for compatibility. If append is the user's reliable composition primitive, remove Open from cards and detail views. Keep structural editing separate and label it as a confirmed new-canvas edit flow.
+3 -3
View File
@@ -233,8 +233,8 @@ Every product-code change in this plugin must finish as one coherent local Git s
`CHANGELOG.md` is public user-facing release communication. Update it only for intentional user-visible release notes; never use it for internal design decisions, implementation details, test output, or agent handoff. Planning-only documentation may be committed separately, but proposals must be labeled clearly and must not be described as implemented runtime behavior.
## 66. Recipe Canvas Loads Must Target the Active Workflow
## 66. Recipe Canvas Actions Use Append as the Safe Composition Path
The current ComfyUI frontend treats `loadGraphData(data)` without a workflow argument as an instruction to create/open a temporary workflow tab. Recipe “Open to canvas” and recipe-edit canvas loads must therefore discover the active workflow from the frontend Pinia store and pass it as the fourth argument. The discovery is deliberately read-only and checks the Vue root's provided Pinia instance, while retaining the legacy three-argument fallback for older ComfyUI builds.
Recipe cards and detail views intentionally expose only “Append to Canvas”. The removed “Open to canvas” action was ambiguous across ComfyUI versions: the legacy API replaced the current graph, while the current workflow-tab API opens a new temporary tab unless an internal active-workflow object is supplied. Keeping only append avoids both meanings and preserves the user's current canvas.
Successful recipe canvas actions also use explicit workspace cleanup: hide the notebook body, recipe view, recipe modal panel, and outer modal before invoking the existing close/media cleanup. This keeps the canvas operation independent from the recipe detail promise and prevents a stale blank workspace shell from remaining visible after a workflow-tab transition.
The separate structural-edit action may still load a recipe into a new canvas for editing; it is not a recipe composition action and remains explicitly confirmed. Successful append actions continue to hide the notebook body, recipe view, recipe modal panel, and outer modal before invoking the existing close/media cleanup.
-6
View File
@@ -125,11 +125,8 @@ export const i18n = {
recipePrompt: '正向提示词',
recipeSteps: '步数',
recipeRestore: '恢复版本',
recipeOpenCanvas: '打开到画布',
recipeAppendCanvas: '追加到画布',
recipeOpenCanvasConfirm: '打开配方会替换当前画布。确定继续吗?',
recipeAppendError: '无法追加此配方到当前画布。',
recipeRestoreError: '恢复工作流配方失败。',
recipeDelete: '删除',
recipeDeleteConfirm: '确定删除这个工作流配方吗?',
recipeDeleteError: '删除工作流配方失败。',
@@ -397,11 +394,8 @@ export const i18n = {
recipePrompt: 'Positive prompt',
recipeSteps: 'Steps',
recipeRestore: 'Restore version',
recipeOpenCanvas: 'Open in Canvas',
recipeAppendCanvas: 'Append to Canvas',
recipeOpenCanvasConfirm: 'Opening this recipe replaces the current canvas. Continue?',
recipeAppendError: 'Could not append this recipe to the current canvas.',
recipeRestoreError: 'Could not restore this Workflow Recipe.',
recipeDelete: 'Delete',
recipeDeleteConfirm: 'Delete this Workflow Recipe?',
recipeDeleteError: 'Could not delete this Workflow Recipe.',
-87
View File
@@ -1,4 +1,3 @@
import { app } from '../../../scripts/app.js';
import { i18n } from './locales.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import {
@@ -31,54 +30,6 @@ function button(parent, label, className = '') {
return element;
}
function canvasMayContainUserWork() {
if (!app.graph?._nodes?.length) return false;
if (app.canvas?.dirty_canvas === false || app.graph?.dirty === false) return false;
return true;
}
function workflowStoreFromPinia(pinia) {
if (!pinia) return null;
if (typeof pinia._s?.get === 'function') return pinia._s.get('workflow') || null;
return pinia.workflow && Object.prototype.hasOwnProperty.call(pinia.workflow, 'activeWorkflow')
? pinia.workflow
: null;
}
function getActiveComfyWorkflow() {
const candidates = [
globalThis.$pinia,
globalThis.pinia,
globalThis.comfyAPI?.pinia,
globalThis.comfyAPI?.app?.pinia,
];
const vueRoot = document.querySelector('#vue-app')?.__vue_app__;
const provides = vueRoot?._context?.provides;
if (provides) {
for (const key of Reflect.ownKeys(provides)) candidates.push(provides[key]);
}
for (const candidate of candidates) {
const store = workflowStoreFromPinia(candidate);
const workflow = store?.activeWorkflow?.value ?? store?.activeWorkflow;
if (workflow) return workflow;
}
return null;
}
export async function loadRecipeWorkflowIntoCurrentCanvas(workflowData) {
const activeWorkflow = getActiveComfyWorkflow();
if (activeWorkflow) {
// ComfyUI 1.45+ uses the fourth argument to distinguish in-place loads
// from opening a new temporary workflow tab.
await app.loadGraphData(workflowData, true, true, activeWorkflow);
return;
}
// Older ComfyUI builds do not expose the workflow store and use the
// three-argument API for the current canvas.
await app.loadGraphData(workflowData);
}
function closeRecipeWorkspace(owner) {
if (!owner) return;
owner.nbPanel && (owner.nbPanel.style.display = 'none');
@@ -100,32 +51,6 @@ async function runRecipeAction(actionButton, action) {
}
}
function missingRecipeNodeTypes(recipe) {
const registry = globalThis.LiteGraph?.registered_node_types;
if (!registry) return [];
return [...new Set((recipe?.workflow?.nodes || [])
.map((node) => node?.type || node?.class_type)
.filter((type) => type && !registry[type]))];
}
export async function openRecipeOnCanvas(owner, recipe) {
const missingTypes = missingRecipeNodeTypes(recipe);
const missingWarning = missingTypes.length
? `\n\n${t('recipeMissingNodes')}:\n${missingTypes.slice(0, 12).join('\n')}`
: '';
if ((canvasMayContainUserWork() || missingWarning) && !await anomalousConfirm(`${t('recipeOpenCanvasConfirm')}${missingWarning}`)) return false;
try {
await loadRecipeWorkflowIntoCurrentCanvas(recipe.workflow);
app.canvas?.setDirty?.(true, true);
closeRecipeWorkspace(owner);
return true;
} catch (error) {
console.error('Could not open Workflow Recipe on canvas:', error);
await anomalousAlert(t('recipeRestoreError'));
return false;
}
}
export async function appendRecipeOnCanvas(owner, recipe) {
try {
appendRecipeToCanvas(recipe);
@@ -839,12 +764,6 @@ function renderOverview(content, owner, recipe, references, finish) {
actions.className = 'anomalous-recipe-actions anomalous-recipe-detail-actions';
const edit = button(actions, t('recipeEdit'), 'anomalous-btn-success');
edit.onclick = () => finish('edit');
const load = button(actions, t('recipeOpenCanvas'), 'anomalous-btn-primary');
load.onclick = () => {
void runRecipeAction(load, async () => {
if (await openRecipeOnCanvas(owner, recipe)) finish('canvas');
});
};
const append = button(actions, t('recipeAppendCanvas'), 'anomalous-btn-primary');
append.onclick = () => {
void runRecipeAction(append, async () => {
@@ -1718,12 +1637,6 @@ export function showRecipeDetail(owner, { recipe, filename, history = [] }) {
}
};
const load = button(headerActions, t('recipeOpenCanvas'), 'anomalous-btn-primary');
load.onclick = () => {
void runRecipeAction(load, async () => {
if (await openRecipeOnCanvas(owner, recipe)) finish('canvas');
});
};
const append = button(headerActions, t('recipeAppendCanvas'), 'anomalous-btn-ghost');
append.onclick = () => {
void runRecipeAction(append, async () => {
+2 -11
View File
@@ -11,8 +11,6 @@ import {
} from './recipe_parser.js';
import {
appendRecipeOnCanvas,
loadRecipeWorkflowIntoCurrentCanvas,
openRecipeOnCanvas,
showRecipeDetail,
} from './ui_recipe_detail.js';
@@ -426,7 +424,7 @@ async function editRecipe(owner, recipe, filename, history = null) {
if (!result || result.mode === 'restored') return;
if (result.mode === 'canvas') {
if (!await anomalousConfirm(t('recipeEditCanvasConfirm'))) return;
await loadRecipeWorkflowIntoCurrentCanvas(editable.workflow);
app.loadGraphData(editable.workflow);
app.canvas?.setDirty?.(true, true);
owner.recipeEditing = { filename, data: editable };
const saveButton = owner.recipeView?.querySelector('[data-recipe-save-current]');
@@ -1106,13 +1104,6 @@ export function renderRecipeList(recipes) {
const primaryActions = document.createElement('div');
primaryActions.className = 'anomalous-recipe-actions-primary';
const restore = appendText(primaryActions, 'button', t('recipeOpenCanvas'), 'anomalous-btn-primary');
restore.type = 'button';
restore.onclick = (e) => { e.stopPropagation(); runRecipeCardAction(restore, async () => {
const data = await fetchRecipeData(recipe.filename);
await openRecipeOnCanvas(this, data);
}, 'recipeRestoreError'); };
const append = appendText(primaryActions, 'button', t('recipeAppendCanvas'), 'anomalous-btn-ghost');
append.type = 'button';
append.onclick = (e) => { e.stopPropagation(); runRecipeCardAction(append, async () => {
@@ -1121,7 +1112,7 @@ export function renderRecipeList(recipes) {
}, 'recipeAppendError'); };
secondaryActions.append(edit, exportButton, remove);
primaryActions.append(append, restore);
primaryActions.append(append);
actions.append(secondaryActions, primaryActions);
card.appendChild(actions);