Multiple node instances (GenerateImageNode, GenerateVideoNode, etc.) were
each making their own /api/models requests when mounted, causing N duplicate
network requests when N nodes existed on the canvas.
This fix:
- Adds deduplicatedFetch utility that shares in-flight requests to the same URL
- Adds useProviderApiKeys() hook with shallow equality for stable API key refs
- Updates all components to use primitive dependencies instead of object refs
- Uses deduplicatedFetch for all model-fetching operations
Expected result: 1 request instead of N when multiple nodes need the same data.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add fetchWithTimeout utility for image save requests (30s timeout)
- Process node images in parallel batches (3 at a time) for faster saves
- Add timeout to waitForPendingImageSyncs (60s) to prevent infinite hangs
- Move isSaving flag reset to finally block for consistent state cleanup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After all video frames are processed (phase 1), the final video duration
is known from highestWrittenTimestamp. Audio is now trimmed to that
exact duration before being encoded into the MP4 (phase 2). Prevents
audio tracks from extending beyond the video.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When videoDuration is 0 (unknown at call time), prepareAudioAsync was
computing targetDuration as max(0.1, 0) = 0.1 seconds, truncating the
audio to 100ms. Now computes the actual decoded audio duration from
buffers and uses that when videoDuration <= 0.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use MediaBunny as the primary decoder following the documented pattern:
getPrimaryAudioTrack() + canDecode() check + AudioBufferSink.buffers().
Falls back to Web Audio API's decodeAudioData if MediaBunny cannot parse
the format (e.g. unusual codecs). This ensures MediaBunny handles
container formats it supports while Web Audio API catches the rest.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MediaBunny's Input/BlobSource is designed for video containers and cannot
parse standalone audio files (MP3, WAV, etc.), causing "unsupported format"
errors at getAudioTracks(). Replace with Web Audio API's decodeAudioData
which natively handles all browser-supported audio formats.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add "video" to Output node's getNodeHandles inputs list
- Consistent with isValidConnection and findCompatibleHandle video->output handling
- Output node now properly accepts VideoStitch connections via all code paths
- Add video handle mapping for videoStitch, generateVideo, and output
- Add audio handle mapping for audioInput and videoStitch
- Connection drop now auto-connects for all handle types
- Extract MIME type from data URL header when fetch() Blob loses it
- Create new Blob with correct type for MediaBunny format detection
- Fix both executeWorkflow and regenerateNode videoStitch paths
- Detect audio file types in handleDragOver (audio/*)
- Handle audio files in handleDrop creating AudioInput nodes
- Show "Drop audio to create node" overlay for audio drag
- Add "audio" to dropType state union
Ensure handles render in all node states (disabled/checking/normal) by
extracting a shared renderHandles function. Move clipOrder syncing from
useMemo side-effect into a proper useEffect. Add dynamic video handle
resolution in WorkflowCanvas for indexed handles. Add VideoStitch
execution case in regenerateNode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add level-based topological sort (groupNodesByLevel) to identify
nodes that can execute concurrently
- Change state from currentNodeId to currentNodeIds[] for tracking
multiple executing nodes
- Execute nodes in batches using Promise.allSettled with configurable
concurrency limit (default 3, adjustable 1-10)
- Integrate AbortController for fail-fast cancellation on errors
- Pass abort signals to API fetch calls for proper request cancellation
- Update UI: multiple nodes show execution border, FloatingActionBar
shows "N nodes" during parallel execution
- Add concurrency slider in Project Settings > Node Defaults tab
- Store concurrency preference in localStorage
Workflows with independent generate nodes (e.g., 3 parallel image
generations) now complete in ~20-25s instead of ~60-90s sequential.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
WorkflowCanvas.tsx:
- Import VideoStitchNode
- Add videoStitch to nodeTypes
- Add getNodeHandles case: inputs=["video", "audio"], outputs=["video"]
- Update isValidConnection to allow video→videoStitch connections
- Add minimap color: orange-500 (#f97316)
ConnectionDropMenu.tsx:
- Add videoStitch to VIDEO_TARGET_OPTIONS (first position)
- Add videoStitch to VIDEO_SOURCE_OPTIONS (produces video output)
- Add videoStitch to AUDIO_TARGET_OPTIONS (accepts audio input)
workflowStore.ts:
- Import VideoStitchNodeData type
- Add videoStitch to getSourceOutput (returns video type)
- Add executeWorkflow case for videoStitch with full stitching logic:
* Check encoder support
* Get connected inputs (videos, audio)
* Convert video data URLs to Blobs
* Prepare audio if connected (using prepareAudioAsync)
* Call stitchVideosAsync with progress callback
* Store output (blob URL for >20MB, data URL otherwise)
* Handle errors
Note: nodeDefaults.ts already had videoStitch dimensions and
createDefaultNodeData from Plan 02, so no changes needed there.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Component with encoder detection on mount (checkEncoderSupport)
- Dynamic video input handles (video-0, video-1, etc.) based on connections
- Audio input handle at bottom (violet color, matching AudioInput)
- Video output handle
- Filmstrip UI with thumbnail extraction (seek to 25% duration)
- Clip ordering via clipOrder array with drag-to-reorder support
- X button per clip to disconnect
- Processing trigger button (disabled until 2+ clips connected)
- Progress overlay during stitching (shows percentage)
- Output video preview on completion
- Disabled state when encoder not supported
- Export added to nodes/index.ts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added AudioInputNode export to nodes/index.ts
- Added audioInput to defaultNodeDimensions (300x200) and createDefaultNodeData
- Registered audioInput in WorkflowCanvas nodeTypes map
- Updated getHandleType to recognize audio handles
- Added audioInput to getNodeHandles (no inputs, audio output)
- Updated isValidConnection for audio-to-audio validation
- Added audio to ConnectionDropState handleType union
- Updated findCompatibleHandle to accept audio type
- Added AUDIO_SOURCE_OPTIONS and AUDIO_TARGET_OPTIONS to ConnectionDropMenu
- Added audio array to getConnectedInputs return type
- Updated getSourceOutput to handle audioInput nodes
- Added audioInput execution case (no-op, data source)
- Added AudioInputNodeData import to workflowStore
- Updated DEFAULT_DIMENSIONS in WorkflowCanvas and quickstart/validation.ts
- Updated createDefaultNodeData in quickstart/validation.ts
- Added violet minimap color (#a78bfa) for audioInput nodes
- File upload via drag-and-drop or click
- Waveform visualization using useAudioVisualization hook
- Play/pause controls with scrub bar
- Canvas-based rendering with ResizeObserver for responsive sizing
- Audio handle (purple/violet) on right side
- Min dimensions: 250px width, 150px height
- Remove button to clear audio file
- Port useAudioVisualization.ts with waveform peak extraction
- Port useAudioMixing.ts with prepareAudioAsync standalone function
- Port useStitchVideos.ts with stitchVideosAsync and checkEncoderSupport
- Export applyFades from useAudioMixing for shared use
- Remove preview mode logic from useStitchVideos (always full quality)
- Inline constants (DEFAULT_BITRATE, MAX_OUTPUT_FPS) in useStitchVideos
- All hooks use MediaBunny for audio/video processing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Install mediabunny@^1.28.0 for video processing
- Add audioInput and videoStitch to NodeType union
- Add AudioInputNodeData interface (audioFile, filename, duration, format)
- Add VideoStitchClip interface for filmstrip items
- Add VideoStitchNodeData interface (clips, clipOrder, outputVideo, progress, encoderSupported)
- Update WorkflowNodeData union to include new node types
- Extend HandleType to include audio and video handles
- Create src/lib/video-encoding.ts with AVC encoding config helpers
GPT Image 1.5 only supports 'aspect_ratio', not 'size'.
Remove stale 'size' values from old workflow data before sending.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add null checks for inputImages arrays that may be undefined on
nodes that haven't been fully initialized, preventing "Cannot read
properties of undefined (reading 'length')" errors during auto-save.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix Kie.ai GPT Image 1.5 failing to retrieve output URL by parsing
resultJson as JSON string (was treated as object incorrectly)
- Add 1x5 grid option to SplitGridNode for vertical contact sheets
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These files are already in .gitignore but were committed
before the rule was added. Kept locally, removed from repo.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These files are already in .gitignore but were committed
before the rule was added. Kept locally, removed from repo.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add onKeyDown handler to PromptNode variable name input so Enter saves.
Add outputGallery, imageCompare, and promptConstructor to ConnectionDropMenu
handle mappings for auto-connect on drag-drop. For imageCompare, redirect
connections to the second handle (image-1) when the first is occupied.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract autocomplete logic to usePromptAutocomplete hook
- Add PromptConstructorEditorModal with portal rendering
- Sync PromptConstructor outputText reactively for downstream nodes
- Add headerButtons prop to BaseNode for custom header actions
- Rename Blank Canvas to New Project with ProjectSetupModal integration
- Auto-push generated images to connected OutputGallery nodes
- Deduplicate images in OutputGallery during workflow execution
- Move PromptNode variable button to header with hover animation
- Extract AvailableVariable interface to types/nodes.ts
- Update tests for onNewProject rename
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test buildEditSystemPrompt basic structure (all major sections present)
- Test with workflow context including node listing and connection formatting
- Test scoped selection with restSummary (node count, type breakdown, boundary connections)
- Test empty restSummary does not show SELECTED SUBSET section
- Test createChatTools returns 3 properly structured tools
- Test execute behavior for answerQuestion, createWorkflow, and editWorkflow
- 21 test cases covering prompt building and tool creation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test all 5 operation types: addNode, removeNode, updateNode, addEdge, removeEdge
- Test skip scenarios for missing nodes/edges with descriptive reasons
- Test batch operations with mixed applied/skipped counts
- Test narrateOperations formatting for all operation types
- 28 test cases covering happy paths and edge cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Two labeled image input handles: 'A' (top 35%) and 'B' (bottom 65%)
- Real-time image collection from connected nodes (same pattern as OutputGalleryNode)
- ReactCompareSlider with horizontal-only slider (portrait=false)
- Corner labels A and B on comparison view
- Placeholder states for 0 or 1 images with connection status feedback
- nodrag nopan nowheel classes for interaction isolation
- BaseNode wrapper with customizable title and comments
- Build verified: zero TypeScript errors
- Install react-compare-slider dependency
- Add imageCompare to NodeType union
- Add ImageCompareNodeData interface (imageA, imageB fields)
- Add to WorkflowNodeData union type
- Register in nodeDefaults with 400x360 dimensions
- Add to WorkflowCanvas nodeTypes and minimap (teal color)
- Add to ConnectionDropMenu IMAGE_TARGET_OPTIONS
- Add executeWorkflow case to assign images[0] to imageA, images[1] to imageB
- Export ImageCompareNode from components/nodes/index
- Add to validation.ts DEFAULT_DIMENSIONS and createDefaultNodeData
- Template textarea with @ autocomplete dropdown
- Detects @ typing and shows matching connected variable names
- Autocomplete dropdown shows @name and prompt value preview
- Arrow keys navigate, Enter/Tab select, Escape closes
- Client-side computation of unresolvedVars from @(\w+) patterns
- Warning badge displays unresolved @tags in amber
- Live preview tooltip on hover showing resolved template
- availableVariables computed from connected prompt nodes with variableName
- Text input handle (target, left) and text output handle (source, right)
- BaseNode wrapper with title 'Prompt Constructor'
- Sync template to store on blur (same pattern as PromptNode)
- Local state prevents cursor jumping during typing
- Filtered autocomplete based on typed characters after @
- Position autocomplete relative to textarea with line height calculation
- Add variableName field to PromptNodeData (optional, backward compatible)
- Add promptConstructor to NodeType union
- Add PromptConstructorNodeData interface (template, outputText, unresolvedVars)
- Add to WorkflowNodeData union type
- Add promptConstructor to defaultNodeDimensions (340x280)
- Add createDefaultNodeData case for promptConstructor
- Modified PromptNode with @ variable naming dialog using createPortal
- Variable name validation: alphanumeric + underscore, max 30 chars
- Blue @ icon when variableName set, dimmed when not set
- Dialog shows variable name input, preview, Save/Clear buttons
- Register PromptConstructorNode in WorkflowCanvas nodeTypes
- Add promptConstructor minimap color (#f472b6 pink)
- Add to TEXT_TARGET_OPTIONS and TEXT_SOURCE_OPTIONS in ConnectionDropMenu
- Add getConnectedInputs support for promptConstructor outputText
- Add executeWorkflow case for promptConstructor:
- Finds connected prompt nodes via text edges
- Builds variable map from variableName + prompt values
- Resolves template by replacing @variableName with values
- Tracks unresolved variables in unresolvedVars array
- Stores outputText and unresolvedVars on node data
- Export PromptConstructorNode from components/nodes/index.ts
- Update duplicate defaultDimensions in WorkflowCanvas and quickstart validation
- Create OutputGalleryNode.tsx with BaseNode wrapper
- Single image input handle (type target, id image)
- Real-time image collection from connected nodes (imageInput, annotation, nanoBanana)
- 3-column scrollable thumbnail grid with nowheel for scroll isolation
- Full-screen lightbox via createPortal with:
- Left/right arrow navigation
- Download button (saves as gallery-image-N.png)
- Close button (X icon, top-right)
- Click backdrop to close
- Keyboard support (Escape, ArrowLeft, ArrowRight)
- Image counter display (N / Total)
- Empty state placeholder when no images connected
- Fix validation.ts: add outputGallery dimensions and createDefaultNodeData case
- Build passes with zero errors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add outputGallery to NodeType union in types/nodes.ts
- Add OutputGalleryNodeData interface with images array field
- Add outputGallery to WorkflowNodeData union
- Register in nodeDefaults: dimensions 320x360, default data with empty images array
- Export OutputGalleryNode from nodes/index.ts
- Register outputGallery in WorkflowCanvas nodeTypes with pink minimap color (#ec4899)
- Add outputGallery to ConnectionDropMenu IMAGE_TARGET_OPTIONS with grid icon
- Add outputGallery case to executeWorkflow in workflowStore
- Add outputGallery to getNodeHandles (image input, no outputs)
- Add outputGallery dimensions to keyboard shortcut default dimensions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add createdAt timestamp to WorkflowEdgeData interface
- Store createdAt on edge creation in onConnect and addEdgeWithType
- Add getImageSequenceNumber helper to EdgeToolbar
- Display "Image N" label for image connections with 2+ edges to same target
- Sequence numbers based on createdAt for stable ordering
- Single connections and text connections show no numbering
The isRunning check and set were separated by an await, allowing a second
call to pass the guard during the async gap. Move set() before the await
to close the window.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Resolve package.json conflict: take master's react-dom ^19.2.4,
keep develop's AI SDK and react-markdown additions.
Regenerate package-lock.json.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Import extractSubgraph from subgraphExtractor
- Parse selectedNodeIds from request body
- Call extractSubgraph before building workflow context
- Pass subgraph.selectedNodes and selectedEdges to buildWorkflowContext
- Update buildEditSystemPrompt to accept optional restSummary parameter
- Add WORKFLOW CONTEXT (SELECTED SUBSET) section when scoped
- Include rest-of-workflow summary with type breakdown and boundary connections
- Add metadata placeholder note to system prompt
- Enhance error handling to return 413 for oversized payloads
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>