Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

113 lines
5.3 KiB
JavaScript

// Workflow ID — read ComfyUI's native workflow UUID.
// UUID dedup on Save-As is handled server-side by _dedup_workflow_uuid.
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
export function getWorkflowId() {
const graph = app.rootGraph ?? app.graph;
return graph?.id || null;
}
// ── workflow id drift healing ────────────────────────────────────
// litegraph regenerates the graph uuid whenever a configure() receives state
// without a top-level `id` (clear() zeroes it, _configureBase mints a fresh
// one) — change-tracker restores and undo hit such paths. The generation
// panel keys history by that uuid, so each silent regeneration "empties" the
// panel and the next file save persists the drifted id.
//
// Detection is anchored to the id INSIDE the workflow's own saved content,
// AND gated on the configure belonging to the active workflow. The gate is
// load-bearing: ComfyUI's afterConfigureGraph hook fires inside loadGraphData
// BEFORE afterLoadNewGraph switches activeWorkflow, so during ANY tab
// open/switch the live graph already wears the INCOMING workflow's id while
// activeWorkflow still points at the OUTGOING one. Comparing that pair is
// indistinguishable from drift by content alone — it's how Save-As cloned
// the source file's history into the new file (sf-test-7 incident) despite
// the file-id anchoring. Undo/redo/reload pass their own workflow to
// loadGraphData (changeTracker.ts), so same-target loads are the only
// legitimate drift candidates; loads targeting another workflow, a string
// tab name (graft tabs), or nothing (PNG drags, templates) never heal.
const wfMeta = new WeakMap(); // ComfyWorkflow → { content, fileId, lastGraphId }
let currentLoad = null; // { targetPath } while app.loadGraphData is on the stack
export function installLoadTargetClassifier() {
if (app.loadGraphData?.__pcrClassified) return;
const orig = app.loadGraphData.bind(app);
app.loadGraphData = async function (...args) {
const wfArg = args[3];
const prev = currentLoad;
currentLoad = { targetPath: (wfArg && typeof wfArg === "object" && wfArg.path) || null };
try {
return await orig(...args);
} finally {
currentLoad = prev;
}
};
app.loadGraphData.__pcrClassified = true;
}
export function healWorkflowIdDrift() {
try {
const graphId = getWorkflowId();
const wf = app.extensionManager?.workflow?.activeWorkflow;
if (!graphId || !wf) return;
// Heal only when this configure reloaded the ACTIVE workflow itself.
// Unknown provenance (classifier missed the load) skips too — a missed
// heal costs one stale panel until the next reconfigure; a wrong clone
// pollutes the DB and breaks the Save-As reset.
if (!currentLoad || currentLoad.targetPath !== wf.path) return;
let meta = wfMeta.get(wf);
if (!meta || meta.content !== wf.content) {
let fileId = null;
try { fileId = JSON.parse(wf.content || wf.originalContent || "null")?.id || null; } catch { /* unparseable content — treat as no persisted identity */ }
meta = { content: wf.content, fileId, lastGraphId: meta?.lastGraphId || null };
wfMeta.set(wf, meta);
}
if (!meta.fileId) return;
if (graphId === meta.fileId) { meta.lastGraphId = graphId; return; } // live matches the file — coherent
// Live graph re-minted away from the file's identity. Heal from the file
// id AND the previous drift segment (gens made between two drifts would
// otherwise fall through), both idempotent server-side.
const froms = [...new Set([meta.fileId, meta.lastGraphId])].filter((x) => x && x !== graphId);
console.warn(`[PromptChain] workflow id drift (${wf.path}): file ${meta.fileId} vs live ${graphId} — re-attaching generation history`);
for (const from of froms) {
api.fetchApi("/promptchain/workflow-clone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from_id: from, to_id: graphId, filepath: wf.path || "" }),
}).catch((e) => console.warn("[PromptChain] id-drift history clone failed", e));
}
meta.lastGraphId = graphId;
} catch (e) {
console.warn("[PromptChain] id drift check failed", e);
}
}
// Intercept workflow saves to detect new files (for sidebar refresh).
// NOTE — deliberate non-feature: Save-As to a NEW file mints a fresh uuid and
// the generation history intentionally RESETS with it (user spec: a new file
// starts a clean timeline). Only the SILENT id changes are healed, by
// healWorkflowIdDrift below — same tab path, regenerated uuid.
export function installSaveInterceptor() {
installLoadTargetClassifier();
const origFetch = api.fetchApi.bind(api);
api.fetchApi = async function(route, options) {
const result = await origFetch(route, options);
if (options?.method === "POST" && typeof route === "string") {
const decoded = decodeURIComponent(route);
if (decoded.includes("/userdata/workflows/") && decoded.split("?")[0].endsWith(".json")) {
const relPath = decoded.split("?")[0].split("/userdata/workflows/").pop();
window.dispatchEvent(new CustomEvent("promptchain:workflows-changed", {
detail: { path: relPath },
}));
}
}
return result;
};
}