From 5a6c8ee44e827f7ba2026f837bd1f6c5e5c4ada0 Mon Sep 17 00:00:00 2001 From: Shrimbly Date: Sun, 22 Feb 2026 20:36:17 +1300 Subject: [PATCH 1/2] fix: output node now auto-loads data when connected and adds Run button - 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 --- src/components/nodes/OutputNode.tsx | 31 ++++++++++++++++++++++++++++- src/store/workflowStore.ts | 5 +++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/nodes/OutputNode.tsx b/src/components/nodes/OutputNode.tsx index 463bf13b..81dc46cb 100644 --- a/src/components/nodes/OutputNode.tsx +++ b/src/components/nodes/OutputNode.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState, useMemo } from "react"; +import { useCallback, useState, useMemo, useEffect, useRef } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useCommentNavigation } from "@/hooks/useCommentNavigation"; @@ -14,7 +14,10 @@ export function OutputNode({ id, data, selected }: NodeProps) { const nodeData = data; const commentNavigation = useCommentNavigation(id); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const edges = useWorkflowStore((state) => state.edges); const [showLightbox, setShowLightbox] = useState(false); + const previousEdgeCountRef = useRef(0); // Determine if content is audio const isAudio = useMemo(() => { @@ -43,6 +46,31 @@ export function OutputNode({ id, data, selected }: NodeProps) { const videoBlobUrl = useVideoBlobUrl(isVideo ? contentSrc ?? null : null); + // Initialize edge count ref on mount + useEffect(() => { + const connectedEdges = edges.filter((edge) => edge.target === id); + previousEdgeCountRef.current = connectedEdges.length; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Auto-trigger execution when connected + useEffect(() => { + const connectedEdges = edges.filter((edge) => edge.target === id); + const currentEdgeCount = connectedEdges.length; + + // Only trigger if we gained a new connection (not on initial mount or disconnect) + if (currentEdgeCount > previousEdgeCountRef.current) { + // Auto-run when a new connection is made + regenerateNode(id); + } + + previousEdgeCountRef.current = currentEdgeCount; + }, [edges, id, regenerateNode]); + + // Handle Run button click + const handleRun = useCallback(() => { + regenerateNode(id); + }, [id, regenerateNode]); + const handleDownload = useCallback(async () => { if (!contentSrc) return; @@ -91,6 +119,7 @@ export function OutputNode({ id, data, selected }: NodeProps) { comment={nodeData.comment} onCustomTitleChange={(title) => updateNodeData(id, { customTitle: title || undefined })} onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })} + onRun={handleRun} selected={selected} className="min-w-[200px]" commentNavigation={commentNavigation ?? undefined} diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 8df3f97e..84579680 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -1167,6 +1167,11 @@ export const useWorkflowStore = create((set, get) => ({ set({ isRunning: false, currentNodeIds: [] }); await logger.endSession(); return; + } else if (node.type === "output") { + await executeOutput(executionCtx); + set({ isRunning: false, currentNodeIds: [] }); + await logger.endSession(); + return; } // After regeneration, execute directly connected downstream consumer nodes From 1075f0daf4e6bf488879f776cbb010d2aeac9742 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Feb 2026 21:12:38 +1300 Subject: [PATCH 2/2] fix: narrow edge subscription, eliminate mount race, and add disabled state to Output node - 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 --- src/components/__tests__/OutputNode.test.tsx | 3 ++ src/components/nodes/OutputNode.tsx | 33 +++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/__tests__/OutputNode.test.tsx b/src/components/__tests__/OutputNode.test.tsx index ed0cf507..7260f34b 100644 --- a/src/components/__tests__/OutputNode.test.tsx +++ b/src/components/__tests__/OutputNode.test.tsx @@ -39,6 +39,9 @@ describe("OutputNode", () => { mockUseWorkflowStore.mockImplementation((selector) => { const state = { updateNodeData: mockUpdateNodeData, + regenerateNode: vi.fn(), + edges: [], + isRunning: false, currentNodeIds: [], groups: {}, nodes: [], diff --git a/src/components/nodes/OutputNode.tsx b/src/components/nodes/OutputNode.tsx index 81dc46cb..83bc1c97 100644 --- a/src/components/nodes/OutputNode.tsx +++ b/src/components/nodes/OutputNode.tsx @@ -15,9 +15,12 @@ export function OutputNode({ id, data, selected }: NodeProps) { const commentNavigation = useCommentNavigation(id); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); - const edges = useWorkflowStore((state) => state.edges); + const connectedEdgeCount = useWorkflowStore( + (state) => state.edges.filter((edge) => edge.target === id).length + ); + const isRunning = useWorkflowStore((state) => state.isRunning); const [showLightbox, setShowLightbox] = useState(false); - const previousEdgeCountRef = useRef(0); + const previousEdgeCountRef = useRef(null); // Determine if content is audio const isAudio = useMemo(() => { @@ -46,25 +49,18 @@ export function OutputNode({ id, data, selected }: NodeProps) { const videoBlobUrl = useVideoBlobUrl(isVideo ? contentSrc ?? null : null); - // Initialize edge count ref on mount - useEffect(() => { - const connectedEdges = edges.filter((edge) => edge.target === id); - previousEdgeCountRef.current = connectedEdges.length; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - // Auto-trigger execution when connected + // Auto-trigger execution when a new connection is made useEffect(() => { - const connectedEdges = edges.filter((edge) => edge.target === id); - const currentEdgeCount = connectedEdges.length; - - // Only trigger if we gained a new connection (not on initial mount or disconnect) - if (currentEdgeCount > previousEdgeCountRef.current) { - // Auto-run when a new connection is made + if (previousEdgeCountRef.current === null) { + // First run — just record the baseline, don't trigger + previousEdgeCountRef.current = connectedEdgeCount; + return; + } + if (connectedEdgeCount > previousEdgeCountRef.current) { regenerateNode(id); } - - previousEdgeCountRef.current = currentEdgeCount; - }, [edges, id, regenerateNode]); + previousEdgeCountRef.current = connectedEdgeCount; + }, [connectedEdgeCount, id, regenerateNode]); // Handle Run button click const handleRun = useCallback(() => { @@ -120,6 +116,7 @@ export function OutputNode({ id, data, selected }: NodeProps) { onCustomTitleChange={(title) => updateNodeData(id, { customTitle: title || undefined })} onCommentChange={(comment) => updateNodeData(id, { comment: comment || undefined })} onRun={handleRun} + isExecuting={isRunning} selected={selected} className="min-w-[200px]" commentNavigation={commentNavigation ?? undefined}