Replace JSON.parse(JSON.stringify(...)) in captureUndoSnapshot with
clonePreservingStrings — a custom deep-clone that creates new
object/array containers but returns string primitives by reference.
Since JS strings are immutable, snapshots safely share base64 blobs
instead of duplicating them. Reduces undo history memory from ~500MB
to ~12.5MB for workflows with 10MB of media data (97.5% reduction).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The selected-node runner was treating audioInput and videoInput as no-ops,
so select-executing these nodes with upstream connections wouldn't pull
connected data. Now matches executeWorkflow's hydration logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix redo shortcut by normalizing event.key to lowercase (Shift makes it "Z")
- Flush pendingDataSnapshot in undo/redo instead of discarding it, preserving
the pre-edit checkpoint so Cmd+Z doesn't skip states
- Recompute dimmedNodeIds after undo/redo to keep dimming in sync
- Set deleteCheckpointActive in removeEdge before clearStaleInputImages to
prevent split undo entries
- Wire videoInput in handleMenuSelect and isValidConnection for video connections
- Validate video MIME type against ACCEPTED_FORMATS whitelist instead of broad regex
- Use URL.createObjectURL(file) for metadata extraction instead of base64→Blob
- Add Shift+Y (Video Input) to keyboard shortcuts dialog
- Update executeNode JSDoc to include videoInput as data-source no-op
- Clear all *Ref/*Refs fields dynamically in clearNodeImageRefs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If a debounced data-edit snapshot was pending when loading or clearing
a workflow, it could fire after undoManager.clear() and push stale
state into the new workflow's undo history. Now both paths cancel the
pending timer and snapshot before clearing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
React Flow v12's deleteElements fires onEdgesChange(remove) BEFORE
onNodesChange(remove), not after. The previous fix assumed the opposite
order, so the microtask-based nodeRemoveCheckpointActive flag was never
true when onEdgesChange ran.
Replace with deleteCheckpointActive — a bidirectional flag set by
whichever handler (edges or nodes) fires first. The second handler
skips its checkpoint. Use setTimeout(0) instead of Promise.resolve()
so the flag survives all microtasks in the current event-loop turn.
Also suppresses debounced undo snapshots from clearStaleInputImages
side effects during the same deletion cycle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When deleting a connected node, clearStaleInputImages calls updateNodeData
on target nodes to clear stale input images. Each updateNodeData call was
creating a debounced undo snapshot, resulting in 3-4 undos needed instead
of 1. Now updateNodeData skips undo tracking when nodeRemoveCheckpointActive
is true, since the node removal checkpoint already captured full pre-deletion state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When deleting a node via the canvas (Delete key), React Flow fires
onNodesChange(remove) then onEdgesChange(remove) synchronously.
Previously this created two undo entries, requiring two undos and
leaving an intermediate state with orphaned edges.
Now onNodesChange sets a microtask-scoped flag that tells onEdgesChange
to skip its checkpoint, collapsing both into a single undo entry.
Added integration test verifying single-undo restores node + edges.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Captures snapshots before all undoable mutations (add/remove nodes,
connect/remove edges, paste, group operations, edge style, drag start).
Data edits are debounced (500ms). Execution outputs are not undoable.
History depth: 50 entries. Cleared on load/clear workflow.
Keyboard shortcuts added to WorkflowCanvas and shortcuts dialog.
15 integration tests covering all undo/redo scenarios through the store.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a new videoInput node type that allows users to drag-and-drop or
browse for video files (MP4, WebM, QuickTime), preview them inline with
native video controls, and feed them into downstream video processing
nodes. Includes full media externalization/hydration support, Shift+Y
keyboard shortcut, and ConnectionDropMenu integration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the local Set variable in executeWorkflow() with direct
reads/writes to the Zustand store field. The dual tracking was
redundant since get() is synchronous and Set.has() is O(1).
Extract a resetSkippedNodes() helper to DRY up the two cleanup blocks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The rebase dropped the commit that re-added skippedNodeIds to the store
interface since develop had already extracted it to a local variable.
However, WorkflowCanvas.tsx needs to observe skippedNodeIds from the
store to apply visual dimming during execution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
skippedNodeIds was in the Zustand store interface but never read by any
component — it was only used as a local variable during executeWorkflow.
Removed the store field/initial state/set() calls, keeping only the local
variable.
Also extracted the duplicate 5-line skip-reset block (success + error paths)
into a resetSkippedNodes() helper inside executeWorkflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The directoryPath field was defined in WorkflowFile and checked during
load, but never written during save. Workflows opened from a different
machine or via file picker had no way to find their externalized media
files, so all ref-based nodes appeared empty.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add node-skipped CSS class (opacity 0.35) for skip dimming
- Expose skippedNodeIds in Zustand store for reactive CSS updates
- Apply node-skipped class in WorkflowCanvas alongside switch-dimmed
- Clear skippedNodeIds on workflow stop, reset, and completion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an optional input node has no data, it and all downstream nodes
with a skipped source are silently skipped during execution. Uses
ANY merge policy — if any source is skipped, the downstream node
is also skipped. Skipped status is reset to idle after workflow
completes or errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pre-subtraction in loadWorkflow was overridden by React Flow's
dimension reconciliation. Instead, correct at the source: in BaseNode's
expand timeout, read _settingsPanelHeight from node data and only add
the delta (measured - saved) so reloaded nodes don't double-count.
Also clear _settingsPanelHeight on collapse to avoid stale values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store measured settings panel height in node data (_settingsPanelHeight).
On workflow load, subtract it from persisted height so BaseNode's expand
effect re-adds the real panel height from a clean baseline instead of
double-counting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On 144Hz+ displays, requestAnimationFrame fires ~144 times/sec. This
adds a throttledRAF utility that limits callback execution to ~60fps
regardless of display refresh rate, normalizing performance across
60Hz, 120Hz, and 144Hz monitors.
Applied to:
- workflowStore hover debounce (event-driven throttle)
- FloatingNodeHeader position tracking (continuous loop throttle)
- ModelParameters ResizeObserver (event-driven throttle)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rapid mouse movement across the canvas fires multiple mouseout events
per frame, each triggering ~150 Zustand selector evaluations. This
coalesces hover updates into one per animation frame and skips them
entirely during canvas panning/dragging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The capabilities prop doesn't exist on ModelSearchDialogProps. Use the
correct initialCapabilityFilter="image" prop instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip Zustand set() when hoveredNodeId already matches the incoming value.
Prevents redundant store updates when the mouse stays on the same node
or when onMouseLeave fires with null when already null.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove solid color header bar from groups, replace with floating rounded-rectangle label at top-left
- Add floating controls toolbar (color picker, lock, delete) at top-right with bg-neutral-800/80 backdrop
- Both label and controls use transform: scale(1/zoom) for constant screen size regardless of canvas zoom
- Remove headerHeight offset from createGroup in workflowStore (no longer reserves vertical space for header)
- Keep all existing group interactions: rename, recolor, lock, delete, drag, resize
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When pasting nodes with inline parameters expanded, the copied
style.height (which included settings panel contribution) was preserved,
causing BaseNode's ResizeObserver to add panel height again on top of the
already-inflated height. Now pasteNodes() resets height to type defaults
while preserving user-set width.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
loadWorkflow now sets showQuickstart: false so the modal doesn't
persist over a loaded project. Previously the isCanvasEmpty gate
handled this implicitly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add hoveredNodeId to store, set from BaseNode mouse events, read in
FloatingNodeHeader. Controls now appear when hovering any part of
the node, not just the floating header strip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After disconnecting an image edge, the target node's inputImages data
was not cleared. On regeneration, useStoredFallback would pick up the
stale images and send them to the API, causing results to still resemble
the old reference. Now removeEdge and onEdgesChange call
clearStaleInputImages to reset inputImages when no image-source edges
remain on the target node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds an evaluationPaused flag that lets users reset evaluation state
and un-dim all downstream paths. The flag auto-resets on workflow
execution or manual rule edits.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The same 20-line block for evaluating rules and updating node data before
executing a ConditionalSwitch appeared in both executeWorkflow and
executeSelectedNodes. Extract to evaluateAndExecuteConditionalSwitch helper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Identical rule evaluation logic existed in ConditionalSwitchNode.tsx and
workflowStore.ts. Extract to src/store/utils/ruleEvaluation.ts and
import from both locations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add geminiApiKey to useProviderApiKeys hook. Update GenerateVideoNode to
include Gemini as a provider when API key is configured, and pass the
X-Gemini-API-Key header when fetching models.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Nano Banana 2 entry to GEMINI_IMAGE_MODELS in model registry
- Add model to GenerateImageNode dropdown with 512px resolution option
- Extend resolution/Google Search gates to support nano-banana-2
- Replace hardcoded model name ternaries with MODEL_DISPLAY_NAMES lookup
in workflowStore, nodeDefaults, and GenerateImageNode migration code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import executeConditionalSwitch in workflowStore
- Add evaluateRuleMatch helper function for rule evaluation
- Add conditionalSwitch case to executeWorkflow with fresh match evaluation
- Add conditionalSwitch case to regenerateNode with same pattern
- Import ConditionalSwitchNodeData and MatchMode in connectedInputs
- Add conditionalSwitch passthrough in getConnectedInputsPure
- Block non-active outputs (isMatched=false) from data flow
- Default output active when no rules match
- Active outputs recursively get upstream text
When paths reconverge downstream of a Switch, dimmed nodes (from disabled
outputs) still held stale data from previous runs. getConnectedInputs now
accepts dimmedNodeIds and skips any source node in the dimmed set, preventing
disabled Switch paths from leaking data into downstream nodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created dimmingUtils.ts with computeDimmedNodes function
- DFS traversal finds all downstream nodes of disabled Switch outputs
- Smart cascade: nodes with active inputs stay active
- Added dimmedNodeIds state to workflowStore
- Added recomputeDimmedNodes action with set equality check
- Recomputes on: switch toggle change, edge add/remove, workflow load
- Resets dimmedNodeIds on clearWorkflow
- Added execution skip logic in executeSingleNode
- Dimmed nodes skip execution but preserve previous output
- Export executeSwitch from execution/index.ts
- Import executeSwitch in workflowStore.ts
- Add switch case to executeWorkflow (after router)
- Add switch case to regenerateNode (after router)
- Import SwitchNodeData in connectedInputs.ts
- Add Switch passthrough logic with toggle filtering in connectedInputs.ts
- Disabled outputs block data flow (no downstream input)
- Enabled outputs recursively traverse upstream (same pattern as Router)
- Edge type determined by switch inputType field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ConnectionDropMenu.tsx:
- Add Router entry to all 8 connection drop menu arrays
- Router appears as last item (utility node) in each menu
- Uses git-branch SVG icon for routing/branching concept
workflowStore.ts:
- Import executeRouter from execution module
- Add "router" case to executeWorkflow switch (after easeCurve)
- Add "router" case to regenerateNode switch (after easeCurve)
execution/index.ts:
- Export executeRouter from simpleNodeExecutors
connectedInputs.ts:
- Add optional visited parameter to getConnectedInputsPure for circular protection
- Add Router passthrough logic before getSourceOutput call
- Router nodes recursively fetch upstream data and route by handle type
- Prevents infinite loops with visited Set tracking
- Handle all 6 data types: image, text, video, audio, 3d, easeCurve
RouterNode.tsx (bugfix):
- Add missing id prop to BaseNode (from 43-01)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Capture prevUnsaved from hasUnsavedChanges before set() call
- Add hasUnsavedChanges to rollback set() call on save failure
- All three fields now properly restored: workflowId, workflowName, hasUnsavedChanges
- Prevents dirty flag from remaining stuck at true after failed save
- Remove directoryPath field from WorkflowFile literal in saveToFile
- Change loadWorkflow fallback chain to exclude workflow.directoryPath
- directoryPath already stored in localStorage via saveSaveConfig()
- Keep optional directoryPath field in WorkflowFile type for backward compatibility
- Old files with directoryPath will still load, new files won't contain it
Zundo-based undo/redo was causing image flickering, failing to restore
deleted nodes, and creating memory issues from large state snapshots.
Removes the entire workflow undo/redo system while keeping the
independent annotation canvas undo/redo intact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap store creation with temporal() middleware
- partialize: Only track nodes (binary-stripped), edges, edgeStyle, groups
- limit: 20 undo steps max
- equality: Fast referential check using undoStateEquality()
- Add keyboard shortcuts:
- Ctrl/Cmd+Z: Undo last change
- Ctrl/Cmd+Shift+Z: Redo
- Ctrl/Cmd+Y: Redo (Windows alt)
- Add drag debouncing to prevent per-pixel snapshots:
- onNodeDragStart: Pause temporal tracking
- onNodeDragStop: Resume tracking and capture final position
- Add _nudgeForSnapshot() helper to force snapshot capture
- Mark workflow as unsaved after undo/redo
- Output Node now automatically triggers execution when a new connection is made
- Added Run button to Output Node header for manual triggering
- Updated regenerateNode to support output node type
- Prevents re-triggering on initial mount or disconnections using edge count tracking
Resolves issue where Output Node would not display data from already-generated upstream nodes unless connected before generation.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fix nanoBananaExecutor narrowing issue where promptText was narrowed
to `never` after type guard, and fix addEdge type mismatch where
Edge<T> data can be undefined but addEdge expects it defined.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Save the previous workflowId and workflowName before mutating state.
If saveToFile() fails, restore the previous values so the workflow
identity is unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of mutating selectedOutputIndex N times in a loop (which races
with React renders), pass { arrayItemIndex: index } as an edge data
override through onConnect. Adds optional edgeDataOverrides parameter
to onConnect and addEdgeWithType.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>