Fix snapshot swap/restore spawning a "Current" snapshot on every browse

Browsing the timeline still created a snapshot per swap — the exact spam
this work set out to remove. Two causes:

1. After loadGraphData, the dedup baseline (lastCapturedHashMap) was
   seeded from the stored record.graphData, but captureSnapshot hashes a
   fresh app.graph.serialize() of the loaded graph. ComfyUI's
   re-serialization isn't byte-identical to the saved bytes, so the hash
   never matched and the pre-swap "Current" capture fired every time.
   Seed the baseline from the live re-serialization instead, so the next
   capture dedupes to a no-op.

2. The pre-swap/pre-restore "Current" capture used skipCosmetic=false, so
   post-load cosmetic settling (node size/position nudges) or a stray move
   still produced a snapshot. Make these automatic captures skip cosmetic
   like the auto path — only explicit manual saves (Ctrl+S / Snapshot
   button) keep cosmetic-only changes now.

Net effect: browsing between snapshots produces zero snapshots; only a
genuine unsaved meaningful edit is preserved before a load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 21:07:29 +02:00
parent 6c2afb1cbb
commit caab0a4eeb
+23 -8
View File
@@ -1778,9 +1778,10 @@ async function restoreSnapshot(record) {
if (!full) { showToast("Failed to load snapshot data", "error"); return; } if (!full) { showToast("Failed to load snapshot data", "error"); return; }
record = full; record = full;
} }
// Preserve any unsaved live edits before replacing the canvas (parity with // Preserve any unsaved meaningful edit before replacing the canvas (parity
// swap). Deduped by hash, so it's a no-op when there is nothing new to save. // with swap). Deduped by hash and skips cosmetic-only changes, so it's a
await captureSnapshot("Current").catch(() => {}); // no-op when there is nothing meaningful new to save.
await captureSnapshot("Current", { skipCosmetic: true }).catch(() => {});
await withRestoreLock(async () => { await withRestoreLock(async () => {
if (!validateSnapshotData(record.graphData)) { if (!validateSnapshotData(record.graphData)) {
showToast("Invalid snapshot data", "error"); showToast("Invalid snapshot data", "error");
@@ -1789,8 +1790,13 @@ async function restoreSnapshot(record) {
try { try {
await app.loadGraphData(record.graphData, true, true); await app.loadGraphData(record.graphData, true, true);
const wfKey = getWorkflowKey(); const wfKey = getWorkflowKey();
lastCapturedHashMap.set(wfKey, quickHash(JSON.stringify(record.graphData))); // Seed the dedup baseline from the LIVE re-serialization, not the
setLastGraphData(wfKey, record.graphData); // stored record: ComfyUI's serialize() of the loaded graph isn't
// byte-identical to the saved bytes, so seeding from record.graphData
// makes the next captureSnapshot() hash mismatch and spuriously save.
const liveGraph = getGraphData() || record.graphData;
lastCapturedHashMap.set(wfKey, quickHash(JSON.stringify(liveGraph)));
setLastGraphData(wfKey, liveGraph);
showToast("Snapshot restored", "success"); showToast("Snapshot restored", "success");
} catch (err) { } catch (err) {
console.warn(`[${EXTENSION_NAME}] Restore failed:`, err); console.warn(`[${EXTENSION_NAME}] Restore failed:`, err);
@@ -1814,8 +1820,11 @@ async function swapSnapshot(record, { quiet = false } = {}) {
// between already-saved snapshots, but it WILL save any unsaved live edits // between already-saved snapshots, but it WILL save any unsaved live edits
// made after a previous swap (when activeSnapshotId is still set) — which // made after a previous swap (when activeSnapshotId is still set) — which
// would otherwise be silently discarded by the load below. // would otherwise be silently discarded by the load below.
// Preserve a genuine unsaved edit before loading, but skip cosmetic-only
// changes (move/resize/collapse) and dedup no-ops so that merely browsing
// between snapshots never spawns a "Current" snapshot (the reported spam).
const prevCurrentId = currentSnapshotId; const prevCurrentId = currentSnapshotId;
const capturedId = await captureSnapshot("Current"); const capturedId = await captureSnapshot("Current", { skipCosmetic: true });
currentSnapshotId = capturedId || prevCurrentId; currentSnapshotId = capturedId || prevCurrentId;
if (!record.graphData) { if (!record.graphData) {
@@ -1833,8 +1842,14 @@ async function swapSnapshot(record, { quiet = false } = {}) {
const workflow = app.extensionManager?.workflow?.activeWorkflow; const workflow = app.extensionManager?.workflow?.activeWorkflow;
await app.loadGraphData(record.graphData, true, true, workflow); await app.loadGraphData(record.graphData, true, true, workflow);
const wfKey = getWorkflowKey(); const wfKey = getWorkflowKey();
lastCapturedHashMap.set(wfKey, quickHash(JSON.stringify(record.graphData))); // Seed the dedup baseline from the LIVE re-serialization, not the
setLastGraphData(wfKey, record.graphData); // stored record: ComfyUI's serialize() of the loaded graph isn't
// byte-identical to the saved bytes, so seeding from record.graphData
// makes the next captureSnapshot() hash mismatch and spuriously save a
// "Current" snapshot on every swap (the reported timeline-swap spam).
const liveGraph = getGraphData() || record.graphData;
lastCapturedHashMap.set(wfKey, quickHash(JSON.stringify(liveGraph)));
setLastGraphData(wfKey, liveGraph);
activeSnapshotId = record.id; activeSnapshotId = record.id;
if (!quiet) showToast("Snapshot swapped", "success"); if (!quiet) showToast("Snapshot swapped", "success");
} catch (err) { } catch (err) {