- Import wouldCreateCycle from executionUtils in WorkflowCanvas
- Add cycle detection in handleConnect for batch and single connections
- Mark backward connections as loop edges (isLoop: true, loopCount: 3)
- Add setLoopCount store action with 1-100 clamping
- Add loop color (#d946ef magenta) to EDGE_COLORS in both files
- Loop edges render in magenta before pause edges
- Add loop indicator overlay (↻ N×) at edge midpoint using foreignObject
- Loop gradient defined in SharedEdgeGradients
When dragging an audio connection and dropping to create a generateVideo
node, the audio input handle was not wired because the case was missing
from handleMenuSelect.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add "audio" to generateVideo's input handles in getNodeHandles() for
connection validation. Update ModelSearchDialog capability filter,
isVideoModel check, and recent models filter to include audio-to-video.
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>
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>
Same approach as Header.tsx — replace browser file picker with native OS
directory picker so loadWorkflow receives the filesystem path for media
hydration from sibling inputs/ and generations/ folders.
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>
Shows a toggle in the FloatingNodeHeader for imageInput, audioInput,
and prompt nodes. When toggled on, the button turns amber and shows
"Optional"; when off, it shows "Required" in the default style.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser hit-testing against large base64 <img> elements is expensive
even when no React state updates occur. Two CSS-level fixes:
1. pointer-events: none on all .react-flow__node img — images never
need direct mouse events (parent containers handle clicks)
2. .canvas-interacting class on <html> during pan/drag disables
pointer-events on all node content via CSS descendant selector,
toggled via direct DOM access (zero React overhead)
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa
When dragging a node over another, onMouseEnter/onMouseLeave were
firing on underlying nodes, updating hoveredNodeId in Zustand and
triggering re-renders of nodes with large base64 images.
- Add isDraggingNodeRef (set on onNodeDragStart, cleared on
onNodeDragStop) to suppress hover events during drag
- Add CSS contain: layout style paint on BaseNode content area to
isolate repaints within each node
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa
Wire up a lightweight callback registry so the floating header can trigger
each generate node's model browse dialog. Moves Browse into the controls
section for consistent vertical alignment and caps title width at 60% to
truncate earlier on narrow nodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace broken `.react-flow__pane.dragging` DOM query guard with a shared
`isPanningRef` set by ReactFlow's onMoveStart/onMoveEnd callbacks. Clear
hoveredNodeId when panning starts. Use a derived boolean selector in
useVideoAutoplay so hovering node A doesn't re-render video nodes B–E.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace broad state.nodes subscriptions in EditableEdge and ReferenceEdge
with narrow Zustand selectors that return primitives, eliminating ~50
unnecessary edge re-renders per node interaction. Share gradient defs
across all edges via SharedEdgeGradients (10 gradients vs 50+ per-edge
defs). Replace SVG blur(6px) filter with layered semi-transparent strokes
for loading glow, avoiding expensive filter evaluation on Windows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WorkflowCanvas, Header, and FloatingActionBar all used
useWorkflowStore() without selectors, subscribing to the entire store.
Every hoveredNodeId change triggered re-renders in all three components
plus a full cascade through edges and nodes from WorkflowCanvas.
- WorkflowCanvas: split into individual selectors for clipboard state
- Header: wrap in useShallow with explicit field selection
- FloatingActionBar: wrap in useShallow with explicit field selection
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>
When dimmedNodeIds is empty and no node has a stale switch-dimmed class,
return the nodes array directly (same reference, zero iteration). The
.map() + regex only runs when dimming is actually active, which is the
uncommon case for most workflows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the monolithic useWorkflowStore() destructure into useShallow for
data fields (nodes, edges, groups, dimmedNodeIds, etc.) and individual
selectors for action references. Actions are stable in Zustand and don't
trigger re-renders, while useShallow ensures the component only re-renders
when data values actually change rather than on every store mutation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline closure callbacks (onCustomTitleChange, onCommentChange,
onRun, onExpand) with stable handler patterns that pass nodeId. Replace
titlePrefix ReactNode prop with primitive provider string. Replace data
object prop with individual primitive fields (isInLockedGroup, isExecuting,
focusedCommentNodeId). This prevents N unnecessary re-renders per frame
during canvas panning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pannable prop to MiniMap for click-to-pan viewport navigation
- Add zoomable prop to MiniMap for scroll-to-zoom on minimap
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When opening the welcome modal over an existing project and choosing
"New Project", the canvas now clears before showing the project setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add click handler to Header.tsx logo/title that calls setShowQuickstart(true)
- Wrap logo and title in button element with hover feedback
- Add onClick to WelcomeModal backdrop that calls onClose
- Add stopPropagation to inner dialog to prevent backdrop close on modal clicks
- Remove isCanvasEmpty gate from WelcomeModal rendering in WorkflowCanvas.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add nodeClickDistance={5} to match nodeDragThreshold, eliminating the
0-5px dead zone where d3-drag suppressed clicks but drags hadn't started.
Stop header pointerdown propagation to prevent pane deselection race.
Fix header drag threshold to use screen pixels for zoom-independent behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- LLMGenerateNode: Remove inline provider/model/parameter controls
- LLMGenerateNode: Convert to full-bleed layout with fullBleed prop
- LLMGenerateNode: Show only text output area in node body
- LLMGenerateNode: Keep floating action buttons (copy, regenerate, clear)
- ControlPanel: Add Max Tokens slider to LLMControls (256-16384 range)
- ControlPanel: Add shadow/gradient overlay on left edge of panel
- WorkflowCanvas: Fix type errors with node.data union types (Rule 1 bug fix)
The header width calculation was preferring node.style.width (set at
creation time, often stale) over node.measured.width (actual DOM size).
This caused headers to be narrower than nodes after image generation
or manual resizing. Now uses measured → style → default fallback chain,
matching the existing pattern used elsewhere in the file.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use node.style.width (number or string), node.measured.width, or
default dimensions — in that priority order. Headers now match the
actual node width after user resize.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Created ControlPanel component rendering on right side of viewport
- Fixed position with pointer-events-none wrapper, auto scrolling
- Filters to single selected configurable node (7 types)
- Placeholder controls for each node type (Task 2 will implement)
- Integrated into WorkflowCanvas after ChatPanel
- Panel appears/disappears based on node selection
- Import ViewportPortal from @xyflow/react
- Add NODE_TITLES mapping for all node types
- Create getNodeTitle helper (checks selectedModel.displayName for generate nodes)
- Create getNodeTitlePrefix helper (returns ProviderBadge for nodes with provider)
- Wire up handleCustomTitleChange and handleCommentChange callbacks
- Render FloatingNodeHeader for each node inside ViewportPortal
- Headers positioned above nodes, width matches node width
- onExpand/onRun callbacks not wired yet (will be added in Plan 03)
- Comment editing and title editing fully functional
The inline defaultDimensions record in the keyboard shortcut handler was
missing the conditionalSwitch entry, causing a TypeScript build error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve three issues with the Conditional Switch node:
- Add fromHandleType resolution for conditionalSwitch in handleConnectEnd so
the connection drop menu appears when dragging from rule/default outputs
- Align output handles with visual rule rows by accounting for the text preview
height in baseOffset and using fixed h-8 rows matching handleSpacing=32
- Move "Add Rule" button after the Default row so the default handle aligns
with its visual row instead of being displaced
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove router/switch/conditionalSwitch entries from WorkflowCanvas
defaultDimensions and validation.ts (these types only exist on develop)
- Add ModelType import and cast in nodeDefaults.ts for type safety
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-existing type error — conditionalSwitch was added to NodeType but the
local defaultDimensions map in keyboard shortcut handler was not updated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import ConditionalSwitchNodeData type in dimmingUtils.ts
- Add conditionalSwitch block to collect potentially dimmed nodes
- Non-matching rules (isMatched=false) trigger downstream dimming
- Default output dims when ANY rule matches (inactive when no match)
- Extend smart cascade blockedTypes collection
- Check conditionalSwitch rules for match status
- Add inactive outputs to blockedTypes
- Extend hasReplacementInput check to skip non-matching outputs
- Guard conditionalSwitch nodes from dimming in WorkflowCanvas
- Updated guard: if (node.type === "switch" || node.type === "conditionalSwitch")
Phase 44's dimming system now handles both Switch and Conditional Switch.
- Import and add ConditionalSwitchNode to nodeTypes map
- Add getNodeHandles case with text input and dynamic outputs
- Add cyan-500 (#06b6d4) minimap color distinct from Router/Switch
- Add isValidConnection text-only validation
- Add handleMenuSelect with text handle and first rule/default source
- Add findCompatibleHandle for text input and rule outputs
- Add ConditionalSwitch to TEXT_TARGET_OPTIONS and TEXT_SOURCE_OPTIONS with filter icon
- Positioned after Switch entry in both menus
The menu path calls onConnect() directly, bypassing handleConnect's
resolveSwitchHandle. This left edges with targetHandle "generic-input"
instead of the actual type, causing inputType to be invalid, handles
to render without color, and downstream connections to fail validation.
- Set targetHandleId to actual type in handleMenuSelect Switch case
- Update Switch inputType immediately when created from menu
- Filter "generic-input" in derivedInputType as safety net
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch output handles use dynamic IDs (random strings) that getHandleType()
can't resolve. Now falls back to the Switch node's inputType field so dragging
from a Switch output shows the correct type-specific connection menu.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Subscribe to dimmedNodeIds in WorkflowCanvas
- Apply switch-dimmed className in allNodes useMemo
- Exclude Switch nodes from dimming (they stay fully visible)
- Added CSS: 40% opacity with 250ms transition
- Transition applies in both directions (dimming and un-dimming)
- Dimming updates reactively when toggles or edges change
- Import and register SwitchNode in nodeTypes
- Add switch case to getNodeHandles (generic-input, dynamic outputs)
- Add resolveSwitchHandle function for input type resolution
- Add Switch-specific validation in isValidConnection
- Add switch case to handleMenuSelect (generic-input target)
- Add switch support to findCompatibleHandle
- Add switch minimap color (#8b5cf6 violet-500)
- Add Switch to all 8 ConnectionDropMenu arrays (IMAGE/TEXT/VIDEO/AUDIO SOURCE/TARGET)
- Import HandleType for type resolution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "switch" to NodeType union in types/nodes.ts
- Create SwitchNodeData interface with inputType and switches array
- Add SwitchNodeData to WorkflowNodeData union
- Register switch dimensions (220x120) in nodeDefaults.ts and validation.ts
- Add createDefaultNodeData case with single default switch
- Implement executeSwitch passthrough executor in simpleNodeExecutors.ts
- Add SwitchNode export to components/nodes/index.ts
- Add switch dimensions to WorkflowCanvas.tsx default dimensions map
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import RouterNode and register in nodeTypes map
- Add router case to getNodeHandles (all 6 types + generic handles)
- Update getHandleType to return null for generic-input/generic-output
- Update isValidConnection to allow router for video/audio/3d/easeCurve
- Add resolveRouterHandle and resolveRouterSourceHandle functions
- Apply router handle resolution in handleConnect (single and batch)
- Update handleMenuSelect with router-specific handle mapping
- Update findCompatibleHandle to support router nodes
- Change Shift+R keyboard shortcut from array to router
- Add router to defaultDimensions (200x80)
- Add router minimap color (#6b7280 neutral-500 gray/slate)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
- 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
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>
- Extract shared video-probing module (constants, helpers, probeVideoMetadata)
from useTrimVideo and useStitchVideos into src/lib/video-probing.ts
- Fix audio interleaving in useTrimVideo to feed audio before video frames,
matching useStitchVideos fix for Discord playback compatibility
- Fix blob URL leaks in VideoTrimNode and videoProcessingExecutors
- Fix nanoBananaExecutor: null coercion for non-string prompts, redact PII
in console.warn, remove duplicate dynamicInputs.prompt
- Fix generateAudioExecutor double updateNodeData on error paths
- Fix fal.ts media fetch ordering for 3D model detection
- Fix kie.ts and replicate.ts content-type detection for audio models
- Fix WorkflowCanvas connection drop menu wiring for generateAudio nodes
- Fix open-file route Windows path quoting
- Add path traversal protection to workflow-images route
- Add AudioInputNode onerror handler for metadata loading failures
- Fix useAudioPlayback uncaught play() promise rejection
- Add generateAudio case to regenerateNode in workflowStore
- Update placeholder Discord links to real invite URL
- Extract ProviderBadge into shared component in Generate3DNode
- Fix VideoTrimNode duration detection with proper cleanup and cancellation
- Fix GenerateAudioNode test fetch mock cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Register VideoFrameGrabNode in node exports, nodeTypes map, and minimap colors
- Add to ConnectionDropMenu as video target (Frame Grab) and image source
- Add executeVideoFrameGrab executor with canvas-based frame extraction
- Wire into all three execution paths: executeWorkflow, regenerateNode, executeSelectedNodes
- Add video connection validation for videoFrameGrab target type
- Add handle mapping for video->videoFrameGrab and videoFrameGrab->image connections
- Add videoFrameGrab case to quickstart validation defaults