78 Commits

Author SHA1 Message Date
Ethanfel 6648d4b9d6 Remove global Alt+Arrow keybinding; guard post-switch seeding
The global Alt+Left/Right keydown handler called preventDefault() on the
browser's Back/Forward shortcut for the whole document — the most likely
cause of "can't switch workflow" after the previous deploy, and a poor
default regardless. Remove the handler (stepToSnapshot remains for a
future conflict-free binding or a timeline button).

Also wrap the post-switch baseline seeding in try/catch so any failure
there can never propagate into ComfyUI's openWorkflow action and block a
workflow switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:18:38 +02:00
Ethanfel caab0a4eeb 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>
2026-06-29 21:07:29 +02:00
Ethanfel 6c2afb1cbb Non-destructive timeline: semantic diffs, quieter autosave, switch-spam fix
Reworks snapshot capture and navigation toward a Fusion-360-style
non-destructive timeline, addressing three pain points.

- Workflow-switch spam: re-seed the dedup/change-detection baseline for
  the newly-opened tab (seedWorkflowBaseline) and suppress auto-capture
  for a short guard window (suppressAutoCapture/SWITCH_GUARD_MS), so the
  graphChanged fired by loadGraphData no longer spawns a redundant
  snapshot of a workflow you only just opened.

- Quieter autosave: detectChangeType now recognises resize, collapse and
  pin (previously saved as "unknown") and classifies pure
  move/resize/collapse as "cosmetic"; a cosmetic flag never escalates a
  real edit. Auto-capture skips cosmetic-only changes; mode (mute/bypass)
  is treated as a meaningful change. The cosmetic gate applies to the
  auto path only — manual saves and the pre-swap/pre-restore "Current"
  capture still preserve everything (no silent layout loss).

- Semantic "what changed": getLiveWidgetNames/widgetNameFor map
  widgets_values indices to widget names (by exact node id), so diffs and
  tooltips read "seed", "text", "cfg" instead of "Value[6]"; the diff
  modal shows meaningful params first and collapses position/size into a
  single muted "Layout" line.

- Non-destructive navigation: Alt+Left/Right step through history via
  stepToSnapshot (quiet, re-entrancy-guarded swap with a position toast);
  jumping between saved states is a storage no-op and never deletes later
  snapshots.

Includes research report + implementation plan under docs/plans.
Verified: node --check passes; 19 unit tests on the diff/classification
logic pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:28:43 +02:00
Ethanfel 0d1415fca4 Audit fixes: data-loss, security, performance, UX + new features
Comprehensive audit pass across the JS frontend and Python backend.

Bugs / correctness:
- Swap & restore now pre-save current state (hash-deduped) so unsaved
  edits aren't lost when swapping/restoring, incl. rapid double-swap
- Unify captureSnapshot/captureNodeSnapshot into _captureCore; node
  captures now update the dedup hash (no duplicate auto-snapshot after)
- Cycle guard in getDisplayPath; Ctrl+S ignores text fields and the
  other-workflow view; tolerant API error parsing; prompt default pre-fill

Security / robustness (backend):
- Validate workflowKey against path traversal (reject ./.. + containment)
- Generic 500 messages (no exception-string leak), logged server-side
- Request body-size cap + migrate record cap
- Atomic writes (temp file + os.replace) on all write paths

Performance / memory:
- /list omits base64 thumbnails (hasThumbnail flag, lazy-loaded client-side)
- LRU-bounded previous-graph cache; persistent (prune+LRU) SVG cache
- Incremental in-place updates for lock/note instead of full list rebuild

UX / docs:
- Busy-op feedback, named-delete confirm, relative timestamps
- README: remove disabled branching feature, fix version badge & storage paths

Features:
- Export / Import snapshots (export route + reuse migrate)
- Storage-usage display (usage route + footer label)
- Pause auto-capture toggle
- Age-based retention (maxAgeDays setting + prune param)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:04:09 +02:00
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
Ethanfel 7782bda677 Fix potential timeline hangs from cycles and refresh cascades
Add cycle detection and O(n) path building to getAllBranches, and a
concurrency guard on timeline refresh to drop overlapping calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:29:58 +01:00
Ethanfel 53b377d2d1 Shrink timeline bar to 50% canvas width
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:46:00 +01:00
Ethanfel 048171cb81 Move snapshot data to ComfyUI user directory
Publish to ComfyUI Registry / Publish Custom Node to Registry (push) Has been cancelled
Store snapshots and profiles under <user_dir>/snapshot_manager/ instead
of <extension>/data/ so user data survives extension updates and reinstalls.
Automatically migrates existing data on first load.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 13:48:40 +01:00
Ethanfel 4a8128b5ce Add per-workflow branching toggle with global default setting
Replace the single global branchingEnabled boolean with a two-tier
system: a ComfyUI settings default and per-workflow overrides via the
sidebar Branch button, persisted independently in localStorage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:55:15 +01:00
Ethanfel 01e09949fb Fix orphaned branches after clearing all snapshots
The footer "Clear All Snapshots" button deleted records from the server
but left stale in-memory state (lastCapturedIdMap, activeSnapshotId,
etc.) intact. New captures then referenced deleted parents, creating
isolated branches. Reset all branching state on both clear-all paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:25:27 +01:00
Ethanfel f9bdc75a2a Add image thumbnail previews for node-triggered snapshots
When SaveSnapshot receives an image tensor, extract a 200x150 JPEG
thumbnail (base64) and include it in the snapshot record. Sidebar shows
a small preview, tooltip displays the generated image instead of SVG,
and the preview modal shows the image above the graph.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:42:51 +01:00
Ethanfel e29e7dfad1 Add capture-in-progress guard to prevent duplicate snapshots
Concurrent captureSnapshot calls (e.g. manual Ctrl+S overlapping a
debounced auto-capture) could both pass the hash check before either
sets the hash, creating duplicates. A simple boolean flag now ensures
only one capture runs at a time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 11:48:45 +01:00
Ethanfel a1c6716ef6 Persist branching and timeline expanded toggles across reload
Store branchingEnabled and timelineExpanded in localStorage. Restore
on init with matching UI state (button class, expand arrow, bar class).
Also persist the forced collapse when branching is disabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:06:48 +01:00
Ethanfel 320ab8647d Protect ancestors of locked snapshots during pruning
Pruning could delete unlocked intermediate nodes between locked
snapshots and the root, creating orphan branches. Now ancestors of
all locked snapshots are added to the protected set in both regular
and node capture paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:39:23 +01:00
Ethanfel fd14d4f1a6 Auto-select branch containing active snapshot on reload
When activeBranchSelections is empty (after reload or tab switch),
selectBranchContaining is called with the effective active ID so the
highlighted branch matches the active snapshot ring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:23:58 +01:00
Ethanfel 0c707b43f3 Add delete button to workflow picker to remove unwanted workflows
Shows × on hover for each workflow row. Confirms before deleting all
snapshots. Clears stale in-memory state for the deleted workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:18:28 +01:00
Ethanfel 463e234cd1 Persist active ring and change detection across reload and tab switch
- Seed lastCapturedIdMap, lastCapturedHashMap, and lastGraphDataMap
  from most recent snapshot on startup so the active ring appears
  immediately after page reload
- Seed lastCapturedIdMap on workflow tab switch so the ring appears
  when switching to a tab with existing snapshots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:11:27 +01:00
Ethanfel 4bfc1912f2 Fix timeline losing active ring, add latest marker, cache repeated calls
- Active ring now falls back to lastCapturedIdMap when activeSnapshotId
  is null (cleared on auto-capture), so the ring persists
- Latest snapshot gets a yellow glow ring for quick identification
- Cache getWorkflowKey() in restore/swap/refresh/populatePicker
- Inline getEffectiveWorkflowKey() to avoid redundant getWorkflowKey()
- Replace double filter() with single loop for record counting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:01:55 +01:00
Ethanfel 74e1e35e6d Revert "Add Load button to workflow picker to open workflows in new tab"
This reverts commit ea69db87b1.
2026-02-26 14:27:26 +01:00
Ethanfel ea69db87b1 Add Load button to workflow picker to open workflows in new tab
Fetches the latest snapshot and loads it as a named temporary workflow
tab, keeping the current workflow untouched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:19:19 +01:00
Ethanfel b933c34c73 Remove workflow sidebar toggle from picker
ComfyUI only supports one sidebar at a time, so opening the
workflows panel just replaces the snapshot manager. Revert to
simple view-only behavior for cross-workflow snapshot browsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:08:18 +01:00
Ethanfel f0e407616e Open workflows sidebar when selecting a different workflow in picker
Instead of trying to programmatically switch the active workflow,
open the ComfyUI workflows sidebar panel so the user can switch
from there. The snapshot picker still shows the selected workflow's
snapshots in view-only mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:02:13 +01:00
Ethanfel ebbdfa4249 Fix workflow picker not switching active workflow
The store's openWorkflow only updates Pinia state without switching
the canvas. Use app.loadGraphData with the workflow object instead,
matching ComfyUI's internal workflow service pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:53:58 +01:00
Ethanfel 24fe846646 Skip shared ancestors in expanded timeline, switch workflow on picker click
Expanded timeline: non-active branch rows now only show their unique
markers after the fork point, indented to align with the active branch.
Removes the dimmed-ancestor approach in favor of cleaner layout.

Workflow picker: clicking a workflow that is open in ComfyUI now
switches to that tab instead of just viewing its snapshots.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:39:14 +01:00
Ethanfel eddf3c6acc Fix inverted expand/collapse arrows on timeline
The timeline grows upward, so show ▴ when collapsed and ▾ when expanded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:05:20 +01:00
Ethanfel 0b0b9d4c39 Add expanded timeline mode to show all branches at once
Adds a toggle button (▾/▴) to the timeline bar that expands it
vertically, displaying every branch as its own row. The active
branch is highlighted, shared ancestors are dimmed on other rows,
and clicking any marker on a non-active row switches to that branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:57:18 +01:00
Ethanfel fa76fbf97a Push arrow glyphs to edges of timeline bar
Top arrow aligns content to flex-start (top edge), bottom arrow to
flex-end (bottom edge), so both arrow tips sit at their respective
edges instead of centering in the available space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:32:22 +01:00
Ethanfel f93441b187 Color timeline branch arrows to match their marker
Arrows inherit the marker's --snap-marker-color so they match the
node color (blue for regular, purple for node snapshots, green for
current, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:31:30 +01:00
Ethanfel e1a48a2456 Make top arrow reach top edge of timeline bar
Fork group fills full timeline height with space-between layout.
Arrow buttons flex to fill remaining space so both arrow tips reach
their respective edges of the timeline bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:26:51 +01:00
Ethanfel 54387c2df4 Fix top arrow position with negative margin
Use margin-bottom:-3px on the top arrow to compensate for the Unicode
glyph's built-in whitespace and pull it tight against the marker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:25:29 +01:00
Ethanfel e4d1c496f2 Fix top arrow to mirror bottom arrow style
Restore 8px size for both arrows. Top arrow uses align-items:flex-end
to push glyph down against marker, bottom arrow uses flex-start to
push glyph up against marker, making them symmetrical.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:21:43 +01:00
Ethanfel f3fab45bf0 Fix timeline top arrow not mirroring bottom arrow position
Reduce arrow button font-size and height from 8px to 6px and set
line-height to 0 so both arrows sit symmetrically tight against the
marker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:20:40 +01:00
Ethanfel d7bd9c4991 Add toggle to disable snapshot branching
New "Branch" button next to "Hide Auto" in the search row. When
toggled off: captures have no parentId, sidebar/timeline show a flat
timestamp-sorted list, branch navigators are hidden, and pruning
skips tree-aware protection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:18:06 +01:00
Ethanfel 284f4e9538 Simplify timeline fork display: remove badge, tighten arrows
Remove the fork count badge from timeline markers and the wrapper div.
Reduce arrow button height and remove extra spacing so the up arrow
sits flush against the marker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:12:41 +01:00
Ethanfel d0057db397 Rework timeline fork display to vertical arrows with badge
Replace horizontal arrow layout with vertical stack: up/down arrows
above and below the marker, with a small blue badge showing the fork
count on the marker node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:07:14 +01:00
Ethanfel d142df5332 Bump version to 3.0.0 and update documentation
Publish to ComfyUI Registry / Publish Custom Node to Registry (push) Has been cancelled
Add documentation for snapshot branching, session profiles,
hide auto-saves toggle, and timeline branch navigation.
Update features list, usage sections, architecture, and FAQ.
Align pyproject.toml and README badge to 3.0.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:59:58 +01:00
Ethanfel 3809af2662 Add hide auto-save toggle to sidebar filter
Toggle button next to the search bar hides Auto and Initial
snapshots to reduce clutter. Filter combines with text search
and persists across refreshes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:53:18 +01:00
Ethanfel bca7e7cf8f Add snapshot branching and profile/session manager
Branching: snapshots now track parentId to form a tree structure.
Swapping to an old snapshot and editing forks into a new branch.
Sidebar and timeline show < 1/3 > navigators at fork points to
switch between branches. Pruning protects ancestors and fork points.
Deleting a fork point re-parents its children.

Profiles: save/load named sets of workflows as session profiles.
Backend stores profiles as JSON in data/profiles/. Sidebar has a
collapsible Profiles section with save, load, and delete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:51:00 +01:00
Ethanfel 7518821447 Add in-memory metadata cache to avoid redundant disk I/O
List, prune, and delete operations now use a lightweight in-memory cache
of snapshot metadata (everything except graphData). Only get_full_record()
and update_meta() hit disk after warm-up, keeping sidebar loads and
auto-capture prune cycles fast.

Key changes:
- snapshot_storage.py: cache layer (_cache, _cache_warmed, _extract_meta,
  _ensure_cached), new get_full_record() and update_meta() functions,
  all existing functions updated to use cache
- snapshot_routes.py: new /get and /update-meta endpoints
- snapshot_manager.js: db_getFullRecord() and db_updateMeta() helpers,
  lazy graphData fetch in restore/swap/diff/preview/tooltip, label/notes/
  lock use update_meta instead of full put to preserve graphData on disk

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:57:41 +01:00
Ethanfel ab3bbc7f71 Refresh timeline after clearing or deleting snapshots
The sidebar refresh didn't trigger a timeline update, leaving stale
markers visible until the next auto-capture or manual action.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:31:03 +01:00