diff --git a/CHANGELOG.md b/CHANGELOG.md index e491e12..d8fb252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is inspired by Keep a Changelog and SemVer. ## [Unreleased] +### Fixed +- **Loading a Koolook workflow whose name collides with an existing + native ComfyUI workflow file no longer breaks ComfyUI's native save.** + Before: loading `MyFlow` from the Kforge Labs sidebar named the + resulting temporary tab `workflows/MyFlow.json`. If the user already + had a native ComfyUI workflow saved at `user/default/workflows/ + MyFlow.json`, hitting ComfyUI's native save threw a confusing + `Error storing user data file 'workflows/MyFlow.json': 409 Conflict` + — because ComfyUI's frontend saves temporary tabs with + `overwrite=false` (see + `Comfy-Org/ComfyUI_frontend/src/stores/userFileStore.ts`), so any + name collision with an existing file 409s. Now: `loadWorkflowOntoCanvas` + HEADs `/userdata/workflows/.json` before loading and, on a hit, + appends ` (Koolook)` to the tab name. The tab still loads, save-back + writes a clean new artifact, and the load toast surfaces the renaming + so the user knows why. Bare `` is preserved when there's no + collision. + ### Changed - **Save selection toast distinguishes "no selection" from "selection points at deleted nodes."** Previously both produced the generic diff --git a/web/sidebar/canvas_io.js b/web/sidebar/canvas_io.js index f370da9..cc809a3 100644 --- a/web/sidebar/canvas_io.js +++ b/web/sidebar/canvas_io.js @@ -104,6 +104,35 @@ export function canvasIsNonEmpty() { } } +// Returns `true` when a workflow file already exists at +// `/userdata/workflows/.json` on the ComfyUI server. +// +// Why we care: workflows we load via `loadGraphData(..., wfName, {})` become +// **temporary** workflow tabs in ComfyUI's frontend. Per +// `Comfy-Org/ComfyUI_frontend/src/stores/userFileStore.ts`, save calls pass +// `overwrite: this.isPersisted` — `false` for temporary workflows. If the +// user clicks ComfyUI's native save on a Koolook-loaded tab whose name +// matches an existing native workflow file, the server returns 409 because +// the POST didn't authorize an overwrite. This pre-load check lets us +// suffix the tab name when a collision exists, so save-back writes a +// clean new artifact instead of 409-ing. +// +// HEAD avoids transferring the file body. If the deployment doesn't +// support HEAD (rare), the catch falls through and we behave as before +// — the user's worst case is the same 409 they already know how to +// work around with Save As. +async function existingNativeWorkflowFileExists(wfName) { + try { + const resp = await fetch( + `/userdata/workflows/${encodeURIComponent(wfName)}.json`, + { method: "HEAD" } + ); + return resp.ok; + } catch (e) { + return false; + } +} + export async function loadWorkflowOntoCanvas(dirPath, wfName) { const graph = getWorkflowGraph(dirPath, wfName); if (!graph) { @@ -120,26 +149,38 @@ export async function loadWorkflowOntoCanvas(dirPath, wfName) { } catch (e) { previousGraph = null; } + // Resolve a collision-free tab name. If a native ComfyUI workflow file + // already exists at `workflows/.json`, append `(Koolook)` so + // ComfyUI's native save flow doesn't 409 (its POST passes + // `overwrite=false` for temporary tabs by design — see comment on + // `existingNativeWorkflowFileExists`). When there's no collision, the + // bare name is used so the tab stays clean. + const collides = await existingNativeWorkflowFileExists(wfName); + const tabName = collides ? `${wfName} (Koolook)` : wfName; try { - // Passing `wfName` as the 4th arg makes loadGraphData create a + // Passing `tabName` as the 4th arg makes loadGraphData create a // temporary workflow tab titled with that name (instead of // "Unsaved Workflow (N)"). Frontend reference: ComfyUI_frontend // src/scripts/app.ts (loadGraphData) → // workflowService.afterLoadNewGraph → createNewTemporary, which - // builds path `workflows/.json` and binds it to the tab. - await app.loadGraphData(graph, true, true, wfName, {}); + // builds path `workflows/.json` and binds it to the tab. + await app.loadGraphData(graph, true, true, tabName, {}); // Defensive fallback for frontends that didn't honor the 4th arg. try { const wf = app.extensionManager?.workflow?.activeWorkflow; - if (wf && wf.isTemporary && typeof wf.rename === "function" && wf.filename !== wfName) { - await wf.rename(`workflows/${wfName}.json`); + if (wf && wf.isTemporary && typeof wf.rename === "function" && wf.filename !== tabName) { + await wf.rename(`workflows/${tabName}.json`); } } catch (e) { console.warn("[Koolook] workflow rename fallback failed:", e); } - toast(`Loaded "${wfName}".`); + if (collides) { + toast(`Loaded "${wfName}" as "${tabName}" — a native workflow file with the original name already exists.`); + } else { + toast(`Loaded "${wfName}".`); + } } catch (e) { console.error("[Koolook] loadGraphData failed:", e); if (previousGraph) {