- Add resolved flag and safeResolve guard pattern from getVideoDimensions
- Add cleanup() function to null event handlers and clear src
- Add 10-second timeout that resolves with null if neither onload nor onerror fires
- Prevents promise from hanging indefinitely on corrupt/malformed images
- Add Shift+T for Audio node to shortcuts list
- Add Shift+R for Array node to shortcuts list
- Both shortcuts already wired in WorkflowCanvas, now visible in dialog
- Add early return in setNodes when height unchanged to prevent unnecessary updates
- Rename shadowed 'selected' variable to 'currentSelection' in validation useEffect
- Reduces redundant React Flow re-renders when array items don't change height
- 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
- Track created blob URL in async chain with createdUrl variable
- Cleanup function revokes orphaned URLs created after cancel flag set
- Remove unnecessary prevInputRef deduplication (React handles this)
- Fixes race where rapid videoUrl changes orphan blob URLs in memory
- 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
- Apply modulo clamping when reading arrayItemIndex from edge data
- Out-of-bounds indices wrap (e.g., index 3 on 2-item array becomes index 1)
- Empty arrays return null instead of crashing
- No edge mutation needed - clamping happens at read time
- Add tests for wrapping behavior and empty array handling
- Create validateWorkflowPath() utility to prevent path traversal attacks
- Reject non-absolute paths, traversal sequences, and dangerous system directories
- Apply validation to workflow and workflow-images POST/GET handlers
- Fix workflow-images ENOENT-specific catch block to match workflow route pattern
- Fix workflow mkdir failure status code from 400 to 500 (server error)
- Add comprehensive path traversal tests for both routes
- All malicious path attempts now return 400 Bad Request
- 3 tests per provider (Replicate, fal.ai): prompt included with dynamicInputs,
no duplication when dynamicInputs already has prompt, existing non-dynamicInputs path works
- Mock fetch intercepts provider API calls and captures request bodies
- Verifies the fix from the previous commit prevents "prompt is required" errors
- Include input.prompt in API request when dynamicInputs are present but don't contain a prompt key
- Use schema paramMap to resolve model-specific prompt parameter name
- Skip if dynamicInputs already provides a prompt value (no duplication)
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>
Replace BINARY_DATA_KEYS (strips all binary) with BINARY_TO_REF mapping
that only strips binary data when a corresponding ref exists (recoverable
from disk). Fields without refs (imageA, imageB, capturedImage, etc.) are
kept in snapshots since they're unrecoverable. After undo/redo, hydrate
missing binary data from refs using existing hydrateWorkflowImages().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VALID_NODE_TYPES was missing audioInput, promptConstructor, outputGallery,
imageCompare, videoStitch, easeCurve, videoTrim, videoFrameGrab, and glbViewer —
all of which are defined in NodeType and have entries in DEFAULT_DIMENSIONS and
createDefaultNodeData. VALID_HANDLE_TYPES was missing video, easeCurve, and 3d
handle types used by EaseCurveNode, GenerateVideoNode, Generate3DNode, GLBViewerNode,
VideoStitchNode, VideoTrimNode, and VideoFrameGrabNode.
Co-Authored-By: Claude Sonnet 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
- Install zundo temporal middleware for Zustand
- Create src/store/undoUtils.ts with:
- BINARY_DATA_KEYS: Set of 20+ binary field names to exclude
- stripBinaryData(): Deep-clone nodes without binary data
- partializeForUndo(): Extract trackable state slice
- undoStateEquality(): Fast referential equality check
- UndoState type for type safety
- Replace broad `state.edges` subscription with count-only selector scoped to this
node's target edges, preventing unnecessary re-renders from unrelated edge changes
- Merge two useEffect hooks into one with a null sentinel ref to prevent false
regeneration on mount when loading saved workflows with existing connections
- Pass isExecuting={isRunning} to BaseNode so the Run button grays out during execution
- Update test mock to include edges, regenerateNode, and isRunning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- Remove Save As button and handler from Header (feature not ready)
- Remove Array node button from FloatingActionBar (not ready for release)
- Add error logging for failed Kie.ai tasks to aid debugging
- Add .superset/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The thumbnail extraction effect had three issues compounding on weak
hardware:
1. The onseeked promise had no timeout — if seek hangs on systems
without hardware video decoding, the entire effect blocks
indefinitely. Added 10-second Promise.race timeout.
2. Created video elements were never cleaned up after use, leaving
phantom decoders running. Added cleanupVideo() that nulls handlers,
clears src, and calls load().
3. The thumbnail cache was cleared wholesale on every run
(thumbnailsRef.current = new Map()), making the cache check on
the next line always miss. Replaced with fingerprint-based
invalidation: only clips whose video data actually changed get
re-extracted, reusing cached thumbnails for unchanged clips.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Data URLs force Chrome to re-parse base64 on each access. With autoPlay
loop, this happens continuously and can freeze the main thread on systems
with weak GPUs that fall back to software video decoding.
Add useVideoBlobUrl hook that converts data URLs to blob URLs via
fetch->blob->createObjectURL. Returns the data URL immediately as
fallback to avoid flicker, then swaps to blob URL once ready (~50ms).
Properly revokes blob URLs on input change and unmount.
Applied to all 6 <video> elements across 5 components:
- GenerateVideoNode (outputVideo)
- VideoStitchNode (outputVideo)
- OutputNode (contentSrc, inline + lightbox)
- EaseCurveNode (outputVideo)
- VideoTrimNode (previewUrl)
Store data is unaffected — getConnectedInputs, executors, and
imageStorage all read outputVideo from the Zustand store, not from
the rendering layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getVideoDimensions created a <video> element for dimension detection but
never cleaned it up, leaving a phantom decoder running alongside the
visible video. On systems with weak GPUs (e.g. Intel HD 5500), this
compounds with autoPlay loop to overwhelm Chrome's main thread.
- Add cleanup helper that nulls handlers, clears src, and calls load()
- Add video.preload = "metadata" to avoid fetching full video content
- Add 10-second timeout that cleans up and resolves null if metadata
never loads (graceful degradation: node keeps current size)
- Apply same cleanup pattern to getImageDimensions
Co-Authored-By: Claude Opus 4.6 <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>
Reject patterns with nested quantifiers (e.g. `(a+)+`) that cause
catastrophic backtracking, cap regex input at 100K chars, and add
lime-400 minimap color for array nodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The clipKey dependency used a presence-only check ("1"/"0"), so when a
video re-generated (truthy→truthy), the effect never re-ran. Additionally,
the ref-based cache keyed by edgeId returned stale thumbnails even if the
effect did fire. Fix by fingerprinting video data via its trailing 20 chars
and clearing the thumbnail cache when clipKey changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The end slider's full-width invisible body (higher z-index) was intercepting
all pointer events, preventing the start slider from receiving input. Fix by
setting pointer-events: none on both range inputs and pointer-events: all on
only the thumb pseudo-elements. Swap z-index so the start handle is on top
when both thumbs overlap at position 0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When importing a workflow via file picker, loadWorkflow had no way to
find the image files on disk because directoryPath was only stored in
localStorage (which doesn't survive browser switches or clears).
Now auto-save embeds directoryPath in the workflow JSON, and
loadWorkflow uses it as a fallback after the workflowPath parameter
and localStorage config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saved workflows can contain multiple edges with the same ID (from
repeated connections or migration normalization). React warns about
duplicate keys and may render edges incorrectly. Deduplicate by
keeping the last occurrence of each edge ID on load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saved workflows could contain edges targeting nanoBanana nodes with
indexed handle IDs (image-0, text-0) from a previous schema-based
handle format. GenerateImageNode now always renders non-indexed
handles (image, text), causing React Flow error #008 on every
interaction. Normalize these edge handles during loadWorkflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Audio from generateAudio was being silently dropped during video
stitching. Add [VideoStitch] checkpoint logs at each stage where
audio can be discarded: getConnectedInputs, prepareAudioAsync,
and codec detection. Surface codec failure as a visible progress
warning instead of only console.warn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The old preview model ID (gemini-2.5-flash-preview-image-generation) was
deprecated. Updated to the current stable ID across provider, cost
calculator, tests, and docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add path traversal guard to workflow-images GET handler (matching POST handler)
- Preserve stored prompt in generateAudioExecutor when connected text is null
- Reset conflicting isVideo/isAudio flags in kie.ts content-type detection
- Fix Infinity aspect ratio causing zero-height canvas in VideoStitchNode
- Check response content-type before trusting isAudioModel in fal.ts
- Move audio feeding inside inner try block in useStitchVideos for proper cleanup
- Add 30s seek timeout to frame extraction in videoProcessingExecutors
- Surface file-open errors via toast in Generate3DNode instead of failing silently
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>