From 13de73a40add6c5ff26f18aea57f86b85a29adee Mon Sep 17 00:00:00 2001 From: malkuthro <166633396+malkuthro@users.noreply.github.com> Date: Tue, 5 May 2026 09:34:26 +0200 Subject: [PATCH] fix(sidebar): avoid 409 on native save when Koolook workflow name collides with an existing native file (#65) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom ------- Loading a workflow from the Kforge Labs sidebar whose name matched an existing native ComfyUI workflow file (e.g. user has both `user/default/workflows/Hi-Res-FIX_v01.json` AND a Koolook entry named `Hi-Res-FIX_v01`) and then clicking ComfyUI's native save button produced a confusing toast: Error storing user data file 'workflows/Hi-Res-FIX_v01.json': 409 Conflict The user's only workaround was to rename via Save As. Root cause ---------- ComfyUI's frontend save flow at `Comfy-Org/ComfyUI_frontend/src/stores/userFileStore.ts`: const resp = await api.storeUserData(this.path, this.content, { overwrite: this.isPersisted, ... }) `overwrite: this.isPersisted` — passes `overwrite=true` only when ComfyUI considers the workflow already persisted to disk. Workflows loaded via the 4th-arg trick on `loadGraphData(..., wfName, {})` become temporary workflows (`isPersisted = false`), so save POSTs with `overwrite=false`. The server (correctly) 409s because a real file already exists at `workflows/.json`. The 409 is **correct ComfyUI behavior** — protecting the user from overwriting an unrelated file with the same name. ComfyUI's frontend has no public API for extensions to mark a workflow as "persisted-from-elsewhere," so we can't make their save flow use overwrite=true for our loaded workflows. Fix --- `loadWorkflowOntoCanvas` now HEADs `/userdata/workflows/.json` before calling `loadGraphData`. - No collision → tab name is the bare `` (existing behavior). - Collision → tab name becomes ` (Koolook)`. Save-back writes `workflows/ (Koolook).json`, which is a clean new artifact that doesn't collide with the user's native file. The load toast surfaces the renaming when it happens, so the user knows their tab was suffixed and why. When the deployment doesn't support HEAD (rare), the catch falls through to the bare name — worst case is the same 409 the user already knows how to work around. Verification ------------ - `node --input-type=module --check` on `canvas_io.js` — clean. - `python tools/preflight_release.py --skip manager-meta -v` PASS. - Synced to live install on Mac via `scripts/sync_to_dev.py` for smoke-test against an actual ComfyUI session. Co-authored-by: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 18 ++++++++++++++ web/sidebar/canvas_io.js | 53 +++++++++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 6 deletions(-) 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) {