Add `isOptional?: boolean` to ImageInputNodeData, AudioInputNodeData,
and PromptNodeData. Add "skipped" to NodeStatus union type to support
skip propagation in the execution engine.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Error overlay persisted on nodes when cycling through history to
previous successful generations. Now resets status/error on carousel
navigation in image, video, and audio generation nodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Saves the base directory to localStorage when creating a project and
prefills it next time the "New Project" modal opens, reducing repetitive
path entry for users who store projects under the same parent directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow marking groups as NBP inputs for workflow export. Adds isNbpInput
flag to NodeGroup, a toggle row in the group context menu with checkmark
indicator, and a dashed border on marked group backgrounds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move fan container from inside the Background row to a child of the
menu div itself, positioned at top-0 left-0. Dots offset by -12px
(half their size) so the fan radiates exactly from the corner.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline horizontal controls with a vertical dropdown menu that
appears above the three-dot button. Menu has icon+label rows for
Background (color picker), Lock/Unlock, and Delete. Color picker fan
anchors from top-left of the menu. Three-dot button is always visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bump button hit areas from w-5/h-5 to w-6/h-6, SVG icons from w-3/h-3
to w-3.5/h-3.5, and three-dot circles from 3px to 4px.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consolidate group controls (color picker, lock/unlock, delete) from the
floating top-right div into a three-dot menu within the zoom-scaled title
container. Controls now scale with zoom like the title label, and are
toggled via a vertical dots button next to the group name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BaseNode: the settings expand branch now clamps the computed height to
minHeight, matching the collapse and ResizeObserver branches.
connectedInputs: resolveTextSourcesThroughRouters now recurses through
switch nodes (same as routers) so upstream text sources are not missed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The left handle was rendered before the image wrapper div in DOM order.
Since the image wrapper has position: relative (needed for its remove
button), it painted over the handle. Moving both handles to render
after visual content ensures they paint last (on top).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nodes with `position: relative` on inner wrappers (e.g. image wrapper
needing it for its remove button) caused left-side handles to render
behind the wrapper due to CSS painting order. Adding z-index: 5 to
all handles ensures they always paint above node content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nodes that passed overflow-clip via contentClassName to BaseNode were
clipping their React Flow handles since handles are children of the
content div. Moved the clip to inner content wrappers that don't
contain handles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use the Lucide split icon (rotated 90deg) for the auto-route button
to better convey the splitting action. Also improve settings reactivity
by reading from Zustand store directly and reparsing atomically on
setting changes.
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>
Variables from upstream Prompt nodes weren't discovered when a Router sat
between the Prompt and PromptConstructor. Added resolveTextSourcesThroughRouters()
helper that recursively traverses router nodes to find actual text sources,
used in both the component and executor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The default content div used overflow-hidden which clipped handles positioned
at left: -7px / right: -7px. Nodes that need clipping (ImageInput, Annotation,
VideoTrim) already set their own contentClassName.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
contain: paint clips rendering at the element boundary like overflow:
hidden, cutting off connection handles. Keep layout and style
containment for performance without the clipping side effect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move removePending to .finally() so it runs on both resolve and reject,
preventing stale rejected promises from accumulating in the pending map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Split button was permanently disabled because sourceImage was only
set during execution, not when an edge was connected. Added a reactive
effect that watches for connected images and updates sourceImage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a router node had multiple output edges to the same downstream node
(e.g., passing both text and image), only the first edge type received
data. The shared _visited set prevented the router from being traversed
a second time, causing subsequent edge types to get empty results.
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
The isDraggingNodeRef check had a race condition: fast mouse movement
could trigger onMouseEnter before React Flow's onNodeDragStart fired.
Adding e.buttons !== 0 catches all cases where a button is held,
which is always true during drag, with no timing dependency.
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
The cache key was using only src.slice(0, 200) which is not unique —
different base64 images share the same MIME prefix and similar headers.
Now samples from multiple positions (start, middle, end) plus length
to virtually eliminate collisions.
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa
Renders low-resolution JPEG thumbnails (256px, q=0.6) in nodes when
zoomed out (effective width < 300px), swapping to full resolution when
zoomed in. Thumbnails are generated eagerly and cached in memory.
- New utility: imageThumbnail.ts (offscreen canvas downscaling)
- New cache: thumbnailCache.ts (ephemeral Map-based cache)
- New hook: useAdaptiveImageSrc (zoom-aware src selector)
- Integrated into all image-bearing nodes (GenerateImage, ImageInput,
Output, Annotation, SplitGrid, VideoFrameGrab, OutputGallery, GLBViewer)
- Lightbox/modal views always use full resolution
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa
Two issues caused nodes to grow taller each time they scrolled back
into view with onlyRenderVisibleElements:
1. InlineParameterPanel initialized contentHeight to 0 and animated
from maxHeight:0 to the measured height on every remount. This CSS
transition caused the ResizeObserver in BaseNode to see the panel
growing and add its height to the node — even though the node
already included it. Fix: when mounted with expanded=true, render
with maxHeight:"none" (no transition) so the panel appears at full
size instantly.
2. The ResizeObserver skip in BaseNode only caught a single observation.
The panel settling could fire multiple times during the CSS
transition. Fix: use a time-based suppression (300ms) that syncs the
tracked height ref without modifying node dimensions for all
observations during the settle window.
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa
The previous fix skipped the expand animation on initial mount but
missed the ResizeObserver path. On remount, trackedSettingsHeightRef
resets to 0, and the ResizeObserver sees the full panel height as a
delta, adding it to the node height that already includes the panel.
Add skipFirstObserveRef: when a node mounts with settings already
expanded, the first ResizeObserver callback syncs the tracked height
without calling setNodes, preventing the cumulative height growth.
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa
When a node with inline parameters scrolls off-screen, React Flow
unmounts it, resetting trackedSettingsHeightRef to 0. On remount the
expand effect would add the panel height to the node again, even though
it was already included. Skip the expand animation on initial mount and
just sync the tracked ref from the DOM instead.
https://claude.ai/code/session_01MvD1n4QeXutgwUpKJuDGHa