From 9daa71ba2a11d2b57a088d43550d1d387ccd69ec Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Tue, 4 Aug 2026 00:29:24 +0800 Subject: [PATCH] fix(recipes): load into active workflow tab --- .agents/logs/ai_changelog.md | 14 +++++++++ .agents/logs/ai_lessons.md | 5 +++- ARCHITECTURE.md | 6 ++++ web/modules/ui_recipe_detail.js | 50 +++++++++++++++++++++++++++++++-- web/modules/ui_recipes.js | 3 +- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/.agents/logs/ai_changelog.md b/.agents/logs/ai_changelog.md index dafb3fe..b482fe0 100644 --- a/.agents/logs/ai_changelog.md +++ b/.agents/logs/ai_changelog.md @@ -544,4 +544,18 @@ - `node --check web/modules/ui_recipe_detail.js` - `node --check web/modules/locales.js` - `node tests/recipe_parser_roundtrip.mjs` +- `python_embeded/python.exe tests/test_recipe_roundtrip.py` +## [Snapshot] 2026-08-04 - Keep recipe canvas loads in the active tab + +**Implemented** + +- Updated recipe Open-to-canvas and recipe-edit canvas loading for the current ComfyUI workflow-tab API. The frontend Pinia workflow store is discovered through the mounted Vue app and the active workflow is passed to `app.loadGraphData()` as its fourth argument, preventing an unintended new temporary tab. +- Kept the legacy three-argument fallback for older ComfyUI builds that do not expose the workflow store. +- Made successful recipe canvas actions explicitly hide notebook content, recipe content, the recipe panel, and the outer plugin modal before running the existing close/media cleanup, preventing a blank workspace shell after the canvas transition. + +**Validation** + +- `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` \ No newline at end of file diff --git a/.agents/logs/ai_lessons.md b/.agents/logs/ai_lessons.md index 22f257e..e6bc431 100644 --- a/.agents/logs/ai_lessons.md +++ b/.agents/logs/ai_lessons.md @@ -439,4 +439,7 @@ Browser `title` hints are delayed, low-contrast, difficult to style, and poor fo **The Solution**: Detect the narrowly defined refresh-only payload, deep-copy the existing recipe, and run the same enrichment and archive/write path as a full update. Keep all other update payloads on complete-recipe validation, and match manual origin edits using stable reference provenance rather than display-only fields. ## 65. Imported Recipe Recovery Must Separate Matching from Applying **The Problem**: An imported workflow stores the author's model path, which is often invalid on the current machine. Treating an exact-path availability refresh as identity matching makes the feature look broken; silently replacing the workflow after a hash match is also unsafe. -**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. \ No newline at end of file +**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. \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3b48201..a7796f2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -232,3 +232,9 @@ Every product-code change in this plugin must finish as one coherent local Git s 5. Leave a clean worktree for the next agent, or document every intentional uncommitted file in the handoff. `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 + +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. + +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. diff --git a/web/modules/ui_recipe_detail.js b/web/modules/ui_recipe_detail.js index 2a39e07..8d75f34 100644 --- a/web/modules/ui_recipe_detail.js +++ b/web/modules/ui_recipe_detail.js @@ -37,8 +37,54 @@ function canvasMayContainUserWork() { 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) { - owner?.nbPanel && (owner.nbPanel.style.display = 'none'); + if (!owner) return; + owner.nbPanel && (owner.nbPanel.style.display = 'none'); + owner.notebookBody && (owner.notebookBody.style.display = 'none'); + owner.recipeView && (owner.recipeView.style.display = 'none'); + owner.modal?.classList.remove('visible'); owner?.close?.(); } @@ -69,7 +115,7 @@ export async function openRecipeOnCanvas(owner, recipe) { : ''; if ((canvasMayContainUserWork() || missingWarning) && !await anomalousConfirm(`${t('recipeOpenCanvasConfirm')}${missingWarning}`)) return false; try { - await app.loadGraphData(recipe.workflow); + await loadRecipeWorkflowIntoCurrentCanvas(recipe.workflow); app.canvas?.setDirty?.(true, true); closeRecipeWorkspace(owner); return true; diff --git a/web/modules/ui_recipes.js b/web/modules/ui_recipes.js index 6abec51..648eaea 100644 --- a/web/modules/ui_recipes.js +++ b/web/modules/ui_recipes.js @@ -11,6 +11,7 @@ import { } from './recipe_parser.js'; import { appendRecipeOnCanvas, + loadRecipeWorkflowIntoCurrentCanvas, openRecipeOnCanvas, showRecipeDetail, } from './ui_recipe_detail.js'; @@ -425,7 +426,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; - app.loadGraphData(editable.workflow); + await loadRecipeWorkflowIntoCurrentCanvas(editable.workflow); app.canvas?.setDirty?.(true, true); owner.recipeEditing = { filename, data: editable }; const saveButton = owner.recipeView?.querySelector('[data-recipe-save-current]');