From 957bfcc724bd15af1fa4a0a6e12395ba698af7aa Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 15 Jan 2026 23:37:41 +1300 Subject: [PATCH 1/4] Fix duplicate input images for generateVideo nodes The generateVideo node type was missing from imageStorage.ts, causing input images to be saved as new files on every workflow save instead of being deduplicated via refs. Co-Authored-By: Claude Opus 4.5 --- src/utils/imageStorage.ts | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/utils/imageStorage.ts b/src/utils/imageStorage.ts index bc9eab44..63768b6b 100644 --- a/src/utils/imageStorage.ts +++ b/src/utils/imageStorage.ts @@ -177,6 +177,36 @@ async function externalizeNodeImages( break; } + case "generateVideo": { + const d = data as import("@/types").GenerateVideoNodeData; + let inputImageRefs = d.inputImageRefs ? [...d.inputImageRefs] : []; + const inputImages: string[] = []; + + // Handle input images array (save to inputs) + // Skip if corresponding inputImageRef already exists + for (let i = 0; i < d.inputImages.length; i++) { + const img = d.inputImages[i]; + const existingRef = d.inputImageRefs?.[i]; + if (existingRef && isBase64DataUrl(img)) { + inputImages.push(""); // Already has ref, just clear the base64 + } else if (isBase64DataUrl(img)) { + const ref = await saveImageAndGetId(img, workflowPath, savedImageIds, "inputs"); + inputImageRefs[i] = ref; + inputImages.push(""); // Empty placeholder + } else { + inputImages.push(img); + } + } + + // Note: outputVideo is a video URL, not saved as an image + newData = { + ...d, + inputImages: inputImages.length > 0 && inputImages.every(i => i === "") ? [] : inputImages, + inputImageRefs: inputImageRefs.length > 0 ? inputImageRefs : undefined, + }; + break; + } + case "output": { const d = data as import("@/types").OutputNodeData; // Output displays generated content, save to generations @@ -374,6 +404,27 @@ async function hydrateNodeImages( break; } + case "generateVideo": { + const d = data as import("@/types").GenerateVideoNodeData; + const inputImages = [...d.inputImages]; + + // Hydrate input images from refs + if (d.inputImageRefs && d.inputImageRefs.length > 0) { + for (let i = 0; i < d.inputImageRefs.length; i++) { + const ref = d.inputImageRefs[i]; + if (ref) { + inputImages[i] = await loadImageById(ref, workflowPath, loadedImages, "inputs"); + } + } + } + + newData = { + ...d, + inputImages, + }; + break; + } + case "output": { const d = data as import("@/types").OutputNodeData; if (d.imageRef && !d.image) { From 03c5acdb0514a9ae1424404d5542f3583474cb87 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 15 Jan 2026 23:38:04 +1300 Subject: [PATCH 2/4] Fix connection to find first unoccupied input handle When connecting to nodes with multiple input handles of the same type, now finds the first unoccupied handle instead of always targeting the first one. Returns null if all handles are occupied. Co-Authored-By: Claude Opus 4.5 --- src/components/WorkflowCanvas.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 425c798f..4d3c9205 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -333,12 +333,22 @@ export function WorkflowCanvas() { const nodeData = node.data as { inputSchema?: Array<{ name: string; type: string }> }; if (nodeData.inputSchema && nodeData.inputSchema.length > 0) { if (needInput) { - // Find first input handle matching the type + // Find input handles matching the type const matchingInputs = nodeData.inputSchema.filter(i => i.type === handleType); - if (matchingInputs.length > 0) { - // Return normalized handle ID, not schema name - // Always use indexed IDs (image-0, text-0) for schema inputs for consistency - return `${handleType}-0`; + const numHandles = matchingInputs.length; + if (numHandles > 0) { + // Find the first unoccupied indexed handle by checking existing edges + for (let i = 0; i < numHandles; i++) { + const candidateHandle = `${handleType}-${i}`; + const isOccupied = edges.some( + (edge) => edge.target === node.id && edge.targetHandle === candidateHandle + ); + if (!isOccupied) { + return candidateHandle; + } + } + // All handles are occupied + return null; } } // Output handle - check for video or image type @@ -425,7 +435,7 @@ export function WorkflowCanvas() { sourceHandleId: fromHandleId, }); }, - [screenToFlowPosition, nodes, handleConnect] + [screenToFlowPosition, nodes, edges, handleConnect] ); // Handle the splitGrid action - uses automated grid detection From 3c6b13c08970ffbc509e15f38eae90ce489bdb79 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 15 Jan 2026 23:40:43 +1300 Subject: [PATCH 3/4] Fix QuickstartInitialView test to match updated UI text Co-Authored-By: Claude Opus 4.5 --- src/components/__tests__/QuickstartInitialView.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/__tests__/QuickstartInitialView.test.tsx b/src/components/__tests__/QuickstartInitialView.test.tsx index 31a89128..09706010 100644 --- a/src/components/__tests__/QuickstartInitialView.test.tsx +++ b/src/components/__tests__/QuickstartInitialView.test.tsx @@ -75,7 +75,7 @@ describe("QuickstartInitialView", () => { expect(screen.getByText("Start from scratch")).toBeInTheDocument(); expect(screen.getByText("Open existing file")).toBeInTheDocument(); expect(screen.getByText("Pre-built workflows")).toBeInTheDocument(); - expect(screen.getByText("Describe what you want")).toBeInTheDocument(); + expect(screen.getByText("Prompt a workflow")).toBeInTheDocument(); }); }); From bdae8e6db8d87ef022d2692d4863a7c7caf0d8a3 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 16 Jan 2026 00:10:12 +1300 Subject: [PATCH 4/4] Show Gemini parameters when Gemini model is selected The aspectRatio, resolution, and useGoogleSearch controls were only shown when isGeminiOnly was true, but that was always false since fal.ai is always available. Now these controls show based on whether the currently selected model is a Gemini model. Co-Authored-By: Claude Opus 4.5 --- src/components/nodes/GenerateImageNode.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index dbbd36f6..5ee59481 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -831,8 +831,8 @@ export function GenerateImageNode({ id, data, selected }: NodeProps )} - {/* Aspect ratio and resolution row - only for Gemini-only mode */} - {isGeminiOnly && ( + {/* Aspect ratio and resolution row - only for Gemini models */} + {currentProvider === "gemini" && (