11 Commits

Author SHA1 Message Date
Ethanfel 8c84d2ff4e Bump version to 3.0.1
Publish to ComfyUI Registry / Publish Custom Node to Registry (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 00:27:33 +02:00
Ethanfel 43fb4c7cef Fix tooltip clamp height and make branch button display conditional 2026-04-03 00:22:32 +02:00
Ethanfel e30d6a1540 Show diff summary in timeline marker tooltip 2026-04-03 00:20:07 +02:00
Ethanfel c11de7f7c3 Show diff summary in sidebar hover tooltip 2026-04-03 00:18:45 +02:00
Ethanfel b591d3e281 Fix widgetValues count for structural widget changes in diff summary 2026-04-03 00:18:01 +02:00
Ethanfel d04731afc6 Compute and store diff summary at capture time 2026-04-03 00:16:35 +02:00
Ethanfel 67fc6d4f7a Skip autosave for move-only changes 2026-04-03 00:14:43 +02:00
Ethanfel ab8860dc7c Hide branchingDefault setting when BRANCHING_ENABLED is false 2026-04-03 00:13:59 +02:00
Ethanfel 04e4095e82 Hide branching UI (BRANCHING_ENABLED = false) 2026-04-03 00:11:49 +02:00
Ethanfel cf8208c540 Add UX rework implementation plan
5 tasks: hide branching, skip move autosaves, compute diff at capture,
show diff in sidebar tooltip, show diff in timeline tooltip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 00:09:11 +02:00
Ethanfel 9946013946 Add UX rework design doc
Covers three changes: skip move-only autosaves, add detailed diff
metadata for hover tooltips, and hide branching UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 00:06:27 +02:00
4 changed files with 455 additions and 4 deletions
+55
View File
@@ -0,0 +1,55 @@
# UX Rework Design — 2026-04-03
## Context
After regular use, three pain points emerged:
1. Autosave generates too many snapshots (moves spam the timeline bar)
2. Branching UI is confusing and never used
3. No way to know what specifically changed when hovering a snapshot
## Changes
### 1. Autosave — Skip move-only snapshots
**Problem:** Moving nodes triggers auto-captures as frequently as structural changes, filling the timeline fast.
**Solution:** Add a guard in the capture path: if `detectChangeType()` returns `'move'`, skip the save and return early. No new config, no debounce changes.
Existing snapshots are unaffected.
### 2. Detailed diff metadata on hover
**Problem:** Snapshot icons show a change type icon but not *what* specifically changed.
**Solution:** At capture time, after `detectChangeType()`, run a new `computeDetailedDiff(prevGraph, currentGraph)` that produces:
```json
{
"added": ["CLIPTextEncode", "KSampler"],
"removed": ["VAEDecode"],
"params": ["KSampler: steps 20→30", "KSampler: cfg 7→9"]
}
```
This object is stored in snapshot metadata. On hover in the sidebar or timeline, a tooltip renders these lines as plain text.
For snapshots captured before this change (no `diff` field), the tooltip falls back to the existing change type label.
**Diff computation:**
- Added/removed nodes: compare node sets by ID, label by title+type
- Changed params: compare `widgets_values` per node between previous and current graph
### 3. Hide branching UI
**Problem:** Branch navigation is confusing and adds visual noise.
**Solution:** Add `const BRANCHING_ENABLED = false` at the top of `snapshot_manager.js`. All branch UI rendering (the `< 1/2 >` navigator, branch buttons, `activeBranchSelections` sidebar/timeline logic) checks this flag and skips when false.
Underlying data and code (parentId, buildSnapshotTree, etc.) are left intact.
## Out of Scope
- Removing branching code
- Making branching a user-facing settings toggle
- Changing debounce timing
- Retroactively hiding or deleting move-type snapshots
+336
View File
@@ -0,0 +1,336 @@
# UX Rework Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Three focused UX improvements: hide branching UI, skip move-only autosaves, and show specific diff details on hover.
**Architecture:** All changes are in `js/snapshot_manager.js`. No backend changes needed. Diff summary is computed at capture time and stored in snapshot metadata so hover display is instant (no extra fetch).
**Tech Stack:** Vanilla JS, ComfyUI extension API, existing `computeDetailedDiff()` function (line 485).
---
### Task 1: Hide branching UI
**Files:**
- Modify: `js/snapshot_manager.js:27` (branchingDefault)
- Modify: `js/snapshot_manager.js:350-354` (isBranchingEnabled)
- Modify: `js/snapshot_manager.js:2847-2866` (branchToggleBtn)
**Step 1: Add BRANCHING_ENABLED constant**
At the very top of the file (after the opening comment block, around line 1-30), add this constant near the other state variables:
```js
const BRANCHING_ENABLED = false;
```
Place it just before line 27 (`let branchingDefault = true;`).
**Step 2: Short-circuit isBranchingEnabled()**
Current code at line 350:
```js
function isBranchingEnabled(wk) {
if (!wk) wk = getEffectiveWorkflowKey();
if (workflowBranchOverrides.has(wk)) return workflowBranchOverrides.get(wk);
return branchingDefault;
}
```
Add an early return at the top of the function:
```js
function isBranchingEnabled(wk) {
if (!BRANCHING_ENABLED) return false;
if (!wk) wk = getEffectiveWorkflowKey();
if (workflowBranchOverrides.has(wk)) return workflowBranchOverrides.get(wk);
return branchingDefault;
}
```
This makes all 14 existing `isBranchingEnabled()` callsites automatically behave as if branching is off — no other changes needed in capture, sidebar, or timeline code.
**Step 3: Hide the branch toggle button in the sidebar**
The button is created at line 2847 and appended at line 2866. Hide it:
Current (line 2847):
```js
const branchToggleBtn = document.createElement("button");
branchToggleBtn.className = "snap-filter-auto-btn" + (isBranchingEnabled() ? " active" : "");
```
Change to:
```js
const branchToggleBtn = document.createElement("button");
branchToggleBtn.className = "snap-filter-auto-btn" + (isBranchingEnabled() ? " active" : "");
branchToggleBtn.style.display = "none";
```
**Step 4: Verify manually in browser**
- Open ComfyUI, open snapshot manager sidebar
- Confirm "Branch" button is not visible
- Create 2-3 snapshots, confirm no branch navigator (`< 1/2 >`) appears
- Confirm timeline has no expand button
**Step 5: Commit**
```bash
git add js/snapshot_manager.js
git commit -m "Hide branching UI (BRANCHING_ENABLED = false)"
```
---
### Task 2: Skip move-only autosaves
**Files:**
- Modify: `js/snapshot_manager.js:1459-1460`
**Step 1: Add move filter in _captureSnapshotInner**
Current code at lines 1458-1460:
```js
const prevGraph = lastGraphDataMap.get(workflowKey);
const changeType = detectChangeType(prevGraph, graphData);
// Determine parentId for branching
```
Add one line after the changeType computation:
```js
const prevGraph = lastGraphDataMap.get(workflowKey);
const changeType = detectChangeType(prevGraph, graphData);
if (changeType === "move") return false;
// Determine parentId for branching
```
**Step 2: Verify manually in browser**
- Open ComfyUI, move several nodes around
- Confirm no new snapshots appear in the sidebar/timeline while moving
- Add a node → confirm a snapshot IS created
- Change a parameter → confirm a snapshot IS created
**Step 3: Commit**
```bash
git add js/snapshot_manager.js
git commit -m "Skip autosave for move-only changes"
```
---
### Task 3: Store diff summary at capture time
**Files:**
- Modify: `js/snapshot_manager.js:614` (after computeDetailedDiff, add new helper)
- Modify: `js/snapshot_manager.js:1471-1481` (record construction in _captureSnapshotInner)
- Modify: `js/snapshot_manager.js:1550-1562` (record construction in captureNodeSnapshot)
**Step 1: Add computeCaptureMetaDiff() and formatCaptureDiffLines() helpers**
Insert these two functions right after `computeDetailedDiff` ends (after line 614, before the SVG section comment at line 616):
```js
// Compact diff stored in snapshot metadata for hover display
function computeCaptureMetaDiff(prevGraph, currGraph) {
if (!prevGraph || !currGraph) return null;
const diff = computeDetailedDiff(prevGraph, currGraph);
const result = {};
if (diff.addedNodes.length > 0)
result.added = diff.addedNodes.map(n => n.title);
if (diff.removedNodes.length > 0)
result.removed = diff.removedNodes.map(n => n.title);
// Nodes with param/property changes (ignore pure position/size changes)
const paramChanged = diff.modifiedNodes.filter(n =>
n.changes.widgetValues || n.changes.properties || n.changes.title || n.changes.mode
);
if (paramChanged.length > 0)
result.params = paramChanged.map(n => {
const count = (n.changes.widgetValues?.length ?? 0) + (n.changes.properties?.length ?? 0);
return count > 0 ? `${n.title} (${count} value${count > 1 ? "s" : ""})` : n.title;
});
if (diff.addedLinks.length > 0 || diff.removedLinks.length > 0)
result.links = { added: diff.addedLinks.length, removed: diff.removedLinks.length };
return Object.keys(result).length > 0 ? result : null;
}
function formatCaptureDiffLines(captureDiff) {
if (!captureDiff) return [];
const lines = [];
if (captureDiff.added?.length)
lines.push(`+ ${captureDiff.added.join(", ")}`);
if (captureDiff.removed?.length)
lines.push(` ${captureDiff.removed.join(", ")}`);
if (captureDiff.params?.length)
lines.push(`~ ${captureDiff.params.join(", ")}`);
if (captureDiff.links) {
const parts = [];
if (captureDiff.links.added) parts.push(`+${captureDiff.links.added} link${captureDiff.links.added > 1 ? "s" : ""}`);
if (captureDiff.links.removed) parts.push(`${captureDiff.links.removed} link${captureDiff.links.removed > 1 ? "s" : ""}`);
if (parts.length) lines.push(parts.join(", "));
}
return lines;
}
```
**Step 2: Add captureDiff to record in _captureSnapshotInner**
Current record construction at lines 1471-1481:
```js
const record = {
id: generateId(),
workflowKey,
timestamp: Date.now(),
label,
nodeCount: nodes.length,
graphData,
locked: false,
changeType,
parentId,
};
```
Add `captureDiff` field:
```js
const captureDiff = computeCaptureMetaDiff(prevGraph, graphData);
const record = {
id: generateId(),
workflowKey,
timestamp: Date.now(),
label,
nodeCount: nodes.length,
graphData,
locked: false,
changeType,
parentId,
...(captureDiff ? { captureDiff } : {}),
};
```
**Step 3: Same for captureNodeSnapshot**
Current record construction at lines 1550-1562:
```js
const record = {
id: generateId(),
workflowKey,
timestamp: Date.now(),
label,
nodeCount: nodes.length,
graphData,
locked: false,
source: "node",
changeType,
parentId,
...(thumbnail ? { thumbnail } : {}),
};
```
Change to:
```js
const captureDiff = computeCaptureMetaDiff(prevGraph, graphData);
const record = {
id: generateId(),
workflowKey,
timestamp: Date.now(),
label,
nodeCount: nodes.length,
graphData,
locked: false,
source: "node",
changeType,
parentId,
...(captureDiff ? { captureDiff } : {}),
...(thumbnail ? { thumbnail } : {}),
};
```
**Step 4: Commit**
```bash
git add js/snapshot_manager.js
git commit -m "Compute and store diff summary at capture time"
```
---
### Task 4: Show diff in sidebar hover tooltip
**Files:**
- Modify: `js/snapshot_manager.js:3573-3603` (mouseenter handler in sidebar)
**Step 1: Add diff lines to tooltip**
Current mouseenter callback (lines 3572-3608) renders an SVG or thumbnail then positions/shows the tooltip. After the SVG/thumbnail is appended (right before the `rect` / positioning block), add diff lines:
Locate this block in the mouseenter callback:
```js
if (!tooltipTimer) return;
const svg = getCachedSVG(rec.id, graphData, { width: 240, height: 180 });
if (!svg) return;
tooltip.appendChild(svg);
}
const rect = item.getBoundingClientRect();
```
Change to:
```js
if (!tooltipTimer) return;
const svg = getCachedSVG(rec.id, graphData, { width: 240, height: 180 });
if (!svg) return;
tooltip.appendChild(svg);
}
// Diff summary lines
const diffLines = formatCaptureDiffLines(rec.captureDiff);
if (diffLines.length > 0) {
const diffEl = document.createElement("div");
diffEl.style.cssText = "margin-top:6px;font-size:11px;line-height:1.5;color:#ccc;white-space:pre;";
diffEl.textContent = diffLines.join("\n");
tooltip.appendChild(diffEl);
}
const rect = item.getBoundingClientRect();
```
**Step 2: Verify manually in browser**
- Create a snapshot by adding a node → hover it in sidebar → tooltip shows SVG preview + diff lines like `+ KSampler`
- Create a snapshot by changing a param → hover it → tooltip shows `~ KSampler (1 value)`
- Hover an old snapshot without captureDiff → tooltip shows SVG only (no crash)
**Step 3: Commit**
```bash
git add js/snapshot_manager.js
git commit -m "Show diff summary in sidebar hover tooltip"
```
---
### Task 5: Show diff in timeline marker tooltip
**Files:**
- Modify: `js/snapshot_manager.js:3715-3717` (buildMarker tip construction)
**Step 1: Append diff lines to marker title**
Current code at lines 3715-3717:
```js
let tip = `${rec.label}${formatTime(rec.timestamp)}\n${iconInfo.label}`;
if (rec.notes) tip += `\n${rec.notes}`;
marker.title = tip;
```
Change to:
```js
let tip = `${rec.label}${formatTime(rec.timestamp)}\n${iconInfo.label}`;
const diffLines = formatCaptureDiffLines(rec.captureDiff);
if (diffLines.length > 0) tip += `\n${diffLines.join("\n")}`;
if (rec.notes) tip += `\n${rec.notes}`;
marker.title = tip;
```
**Step 2: Verify manually in browser**
- Hover a timeline marker → native tooltip shows label, time, change type, then diff lines
**Step 3: Commit**
```bash
git add js/snapshot_manager.js
git commit -m "Show diff summary in timeline marker tooltip"
```
+63 -3
View File
@@ -24,6 +24,7 @@ let autoCaptureEnabled = true;
let captureOnLoad = true; let captureOnLoad = true;
let maxNodeSnapshots = 5; let maxNodeSnapshots = 5;
let showTimeline = false; let showTimeline = false;
const BRANCHING_ENABLED = false;
let branchingDefault = true; // updated by ComfyUI settings onChange let branchingDefault = true; // updated by ComfyUI settings onChange
// ─── State ─────────────────────────────────────────────────────────── // ─── State ───────────────────────────────────────────────────────────
@@ -348,6 +349,7 @@ function getEffectiveWorkflowKey() {
} }
function isBranchingEnabled(wk) { function isBranchingEnabled(wk) {
if (!BRANCHING_ENABLED) return false;
if (!wk) wk = getEffectiveWorkflowKey(); if (!wk) wk = getEffectiveWorkflowKey();
if (workflowBranchOverrides.has(wk)) return workflowBranchOverrides.get(wk); if (workflowBranchOverrides.has(wk)) return workflowBranchOverrides.get(wk);
return branchingDefault; return branchingDefault;
@@ -613,6 +615,48 @@ function computeDetailedDiff(baseGraph, targetGraph) {
}; };
} }
// Compact diff stored in snapshot metadata for hover display
function computeCaptureMetaDiff(prevGraph, currGraph) {
if (!prevGraph || !currGraph) return null;
const diff = computeDetailedDiff(prevGraph, currGraph);
const result = {};
if (diff.addedNodes.length > 0)
result.added = diff.addedNodes.map(n => n.title);
if (diff.removedNodes.length > 0)
result.removed = diff.removedNodes.map(n => n.title);
// Nodes with param/property changes (ignore pure position/size changes)
const paramChanged = diff.modifiedNodes.filter(n =>
n.changes.widgetValues || n.changes.properties || n.changes.title || n.changes.mode
);
if (paramChanged.length > 0)
result.params = paramChanged.map(n => {
const wvCount = Array.isArray(n.changes.widgetValues) ? n.changes.widgetValues.length : (n.changes.widgetValues ? 1 : 0);
const count = wvCount + (n.changes.properties?.length ?? 0);
return count > 0 ? `${n.title} (${count} value${count > 1 ? "s" : ""})` : n.title;
});
if (diff.addedLinks.length > 0 || diff.removedLinks.length > 0)
result.links = { added: diff.addedLinks.length, removed: diff.removedLinks.length };
return Object.keys(result).length > 0 ? result : null;
}
function formatCaptureDiffLines(captureDiff) {
if (!captureDiff) return [];
const lines = [];
if (captureDiff.added?.length)
lines.push(`+ ${captureDiff.added.join(", ")}`);
if (captureDiff.removed?.length)
lines.push(` ${captureDiff.removed.join(", ")}`);
if (captureDiff.params?.length)
lines.push(`~ ${captureDiff.params.join(", ")}`);
if (captureDiff.links) {
const parts = [];
if (captureDiff.links.added) parts.push(`+${captureDiff.links.added} link${captureDiff.links.added > 1 ? "s" : ""}`);
if (captureDiff.links.removed) parts.push(`${captureDiff.links.removed} link${captureDiff.links.removed > 1 ? "s" : ""}`);
if (parts.length) lines.push(parts.join(", "));
}
return lines;
}
// ─── SVG Graph Renderer ───────────────────────────────────────────── // ─── SVG Graph Renderer ─────────────────────────────────────────────
const SVG_NS = "http://www.w3.org/2000/svg"; const SVG_NS = "http://www.w3.org/2000/svg";
@@ -1457,6 +1501,7 @@ async function _captureSnapshotInner(label) {
const prevGraph = lastGraphDataMap.get(workflowKey); const prevGraph = lastGraphDataMap.get(workflowKey);
const changeType = detectChangeType(prevGraph, graphData); const changeType = detectChangeType(prevGraph, graphData);
if (changeType === "move") return false;
// Determine parentId for branching // Determine parentId for branching
let parentId = null; let parentId = null;
@@ -1468,6 +1513,7 @@ async function _captureSnapshotInner(label) {
} }
} }
const captureDiff = computeCaptureMetaDiff(prevGraph, graphData);
const record = { const record = {
id: generateId(), id: generateId(),
workflowKey, workflowKey,
@@ -1478,6 +1524,7 @@ async function _captureSnapshotInner(label) {
locked: false, locked: false,
changeType, changeType,
parentId, parentId,
...(captureDiff ? { captureDiff } : {}),
}; };
try { try {
@@ -1547,6 +1594,7 @@ async function captureNodeSnapshot(label = "Node Trigger", thumbnail = null) {
} }
} }
const captureDiff = computeCaptureMetaDiff(prevGraph, graphData);
const record = { const record = {
id: generateId(), id: generateId(),
workflowKey, workflowKey,
@@ -1558,6 +1606,7 @@ async function captureNodeSnapshot(label = "Node Trigger", thumbnail = null) {
source: "node", source: "node",
changeType, changeType,
parentId, parentId,
...(captureDiff ? { captureDiff } : {}),
...(thumbnail ? { thumbnail } : {}), ...(thumbnail ? { thumbnail } : {}),
}; };
@@ -2846,6 +2895,7 @@ async function buildSidebar(el) {
const branchToggleBtn = document.createElement("button"); const branchToggleBtn = document.createElement("button");
branchToggleBtn.className = "snap-filter-auto-btn" + (isBranchingEnabled() ? " active" : ""); branchToggleBtn.className = "snap-filter-auto-btn" + (isBranchingEnabled() ? " active" : "");
branchToggleBtn.style.display = BRANCHING_ENABLED ? "" : "none";
branchToggleBtn.textContent = "Branch"; branchToggleBtn.textContent = "Branch";
branchToggleBtn.title = "Toggle snapshot branching"; branchToggleBtn.title = "Toggle snapshot branching";
branchToggleBtn.addEventListener("click", async () => { branchToggleBtn.addEventListener("click", async () => {
@@ -3590,12 +3640,20 @@ async function buildSidebar(el) {
if (!svg) return; if (!svg) return;
tooltip.appendChild(svg); tooltip.appendChild(svg);
} }
// Diff summary lines
const diffLines = formatCaptureDiffLines(rec.captureDiff);
if (diffLines.length > 0) {
const diffEl = document.createElement("div");
diffEl.style.cssText = "margin-top:6px;font-size:11px;line-height:1.5;color:#ccc;white-space:pre;";
diffEl.textContent = diffLines.join("\n");
tooltip.appendChild(diffEl);
}
const rect = item.getBoundingClientRect(); const rect = item.getBoundingClientRect();
let left = rect.right + 8; let left = rect.right + 8;
let top = rect.top; let top = rect.top;
// Clamp to viewport // Clamp to viewport
if (left + 260 > window.innerWidth) left = rect.left - 260; if (left + 260 > window.innerWidth) left = rect.left - 260;
if (top + 200 > window.innerHeight) top = window.innerHeight - 200; if (top + 280 > window.innerHeight) top = window.innerHeight - 280;
if (top < 0) top = 0; if (top < 0) top = 0;
tooltip.style.left = `${left}px`; tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`; tooltip.style.top = `${top}px`;
@@ -3713,6 +3771,8 @@ function buildTimeline() {
if (isLatest) marker.classList.add("snap-timeline-marker-latest"); if (isLatest) marker.classList.add("snap-timeline-marker-latest");
let tip = `${rec.label}${formatTime(rec.timestamp)}\n${iconInfo.label}`; let tip = `${rec.label}${formatTime(rec.timestamp)}\n${iconInfo.label}`;
const diffLines = formatCaptureDiffLines(rec.captureDiff);
if (diffLines.length > 0) tip += `\n${diffLines.join("\n")}`;
if (rec.notes) tip += `\n${rec.notes}`; if (rec.notes) tip += `\n${rec.notes}`;
marker.title = tip; marker.title = tip;
@@ -3947,7 +4007,7 @@ if (window.__COMFYUI_FRONTEND_VERSION__) {
if (value && timelineRefresh) timelineRefresh().catch(() => {}); if (value && timelineRefresh) timelineRefresh().catch(() => {});
}, },
}, },
{ ...(BRANCHING_ENABLED ? [{
id: "SnapshotManager.branchingDefault", id: "SnapshotManager.branchingDefault",
name: "Enable branching by default", name: "Enable branching by default",
type: "boolean", type: "boolean",
@@ -3958,7 +4018,7 @@ if (window.__COMFYUI_FRONTEND_VERSION__) {
if (sidebarRefresh) sidebarRefresh().catch(() => {}); if (sidebarRefresh) sidebarRefresh().catch(() => {});
if (timelineRefresh) timelineRefresh().catch(() => {}); if (timelineRefresh) timelineRefresh().catch(() => {});
}, },
}, }] : []),
], ],
init() { init() {
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-snapshot-manager" name = "comfyui-snapshot-manager"
description = "Automatically snapshots workflow state with a sidebar to browse and restore previous versions." description = "Automatically snapshots workflow state with a sidebar to browse and restore previous versions."
version = "3.1.0" version = "3.0.1"
license = {text = "MIT"} license = {text = "MIT"}
[project.urls] [project.urls]