Compare commits

..

2 Commits

Author SHA1 Message Date
Vikhyath Mondreti
4cda2b4eba fix deployed state fallback 2026-01-24 02:49:37 -08:00
Vikhyath Mondreti
bd2838a88c fix(notes): ghost edges 2026-01-24 02:44:52 -08:00
3 changed files with 58 additions and 32 deletions

View File

@@ -81,6 +81,7 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState } from '@/stores/workflows/workflow/types'
/** Lazy-loaded components for non-critical UI that can load after initial render */
const LazyChat = lazy(() =>
@@ -535,8 +536,7 @@ const WorkflowContent = React.memo(() => {
return edgesToFilter.filter((edge) => {
const sourceBlock = blocks[edge.source]
const targetBlock = blocks[edge.target]
if (!sourceBlock || !targetBlock) return false
return !isAnnotationOnlyBlock(sourceBlock.type) && !isAnnotationOnlyBlock(targetBlock.type)
return Boolean(sourceBlock && targetBlock)
})
}, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks])
@@ -1097,6 +1097,13 @@ const WorkflowContent = React.memo(() => {
[collaborativeBatchRemoveEdges]
)
const isAutoConnectSourceCandidate = useCallback((block: BlockState): boolean => {
if (!block.enabled) return false
if (block.type === 'response') return false
if (isAnnotationOnlyBlock(block.type)) return false
return true
}, [])
/** Finds the closest block to a position for auto-connect. */
const findClosestOutput = useCallback(
(newNodePosition: { x: number; y: number }): BlockData | null => {
@@ -1109,8 +1116,7 @@ const WorkflowContent = React.memo(() => {
position: { x: number; y: number }
distanceSquared: number
} | null>((acc, [id, block]) => {
if (!block.enabled) return acc
if (block.type === 'response') return acc
if (!isAutoConnectSourceCandidate(block)) return acc
const node = nodeIndex.get(id)
if (!node) return acc
@@ -1140,7 +1146,7 @@ const WorkflowContent = React.memo(() => {
position: closest.position,
}
},
[blocks, getNodes, getNodeAnchorPosition, isPointInLoopNode]
[blocks, getNodes, getNodeAnchorPosition, isPointInLoopNode, isAutoConnectSourceCandidate]
)
/** Determines the appropriate source handle based on block type. */
@@ -1208,7 +1214,8 @@ const WorkflowContent = React.memo(() => {
position: { x: number; y: number }
distanceSquared: number
} | null>((acc, block) => {
if (block.type === 'response') return acc
const blockState = blocks[block.id]
if (!blockState || !isAutoConnectSourceCandidate(blockState)) return acc
const distanceSquared =
(block.position.x - targetPosition.x) ** 2 + (block.position.y - targetPosition.y) ** 2
if (!acc || distanceSquared < acc.distanceSquared) {
@@ -1225,7 +1232,7 @@ const WorkflowContent = React.memo(() => {
}
: undefined
},
[]
[blocks, isAutoConnectSourceCandidate]
)
/**
@@ -2364,7 +2371,6 @@ const WorkflowContent = React.memo(() => {
if (!sourceNode || !targetNode) return
// Prevent connections to/from annotation-only blocks (non-executable)
if (
isAnnotationOnlyBlock(sourceNode.data?.type) ||
isAnnotationOnlyBlock(targetNode.data?.type)
@@ -2372,13 +2378,8 @@ const WorkflowContent = React.memo(() => {
return
}
// Prevent incoming connections to trigger blocks (webhook, schedule, etc.)
if (targetNode.data?.config?.category === 'triggers') {
return
}
// Prevent incoming connections to starter blocks (still keep separate for backward compatibility)
if (targetNode.data?.type === 'starter') {
const targetBlock = blocks[targetNode.id]
if (targetBlock && TriggerUtils.isTriggerBlock(targetBlock)) {
return
}

View File

@@ -1125,17 +1125,6 @@ const sseHandlers: Record<string, SSEHandler> = {
await get().handleNewChatCreation(context.newChatId)
}
},
title_updated: (_data, _context, get, set) => {
const title = _data.title
if (!title) return
const { currentChat, chats } = get()
if (currentChat) {
set({
currentChat: { ...currentChat, title },
chats: chats.map((c) => (c.id === currentChat.id ? { ...c, title } : c)),
})
}
},
tool_result: (data, context, get, set) => {
try {
const toolCallId: string | undefined = data?.toolCallId || data?.data?.id

View File

@@ -4,9 +4,10 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
import { isAnnotationOnlyBlock, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { filterNewEdges, getUniqueBlockName, mergeSubblockState } from '@/stores/workflows/utils'
@@ -90,6 +91,38 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown {
return null
}
function isValidEdge(
edge: Edge,
blocks: Record<string, { type: string; triggerMode?: boolean }>
): boolean {
const sourceBlock = blocks[edge.source]
const targetBlock = blocks[edge.target]
if (!sourceBlock || !targetBlock) return false
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
if (isAnnotationOnlyBlock(targetBlock.type)) return false
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
return true
}
function filterValidEdges(
edges: Edge[],
blocks: Record<string, { type: string; triggerMode?: boolean }>
): Edge[] {
return edges.filter((edge) => {
const valid = isValidEdge(edge, blocks)
if (!valid) {
logger.warn('Filtered invalid edge', {
edgeId: edge.id,
source: edge.source,
target: edge.target,
sourceType: blocks[edge.source]?.type,
targetType: blocks[edge.target]?.type,
})
}
return valid
})
}
const initialState = {
blocks: {},
edges: [],
@@ -360,8 +393,9 @@ export const useWorkflowStore = create<WorkflowStore>()(
}
if (edges && edges.length > 0) {
const validEdges = filterValidEdges(edges, newBlocks)
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
for (const edge of edges) {
for (const edge of validEdges) {
if (!existingEdgeIds.has(edge.id)) {
newEdges.push({
id: edge.id || crypto.randomUUID(),
@@ -495,8 +529,11 @@ export const useWorkflowStore = create<WorkflowStore>()(
},
batchAddEdges: (edges: Edge[]) => {
const blocks = get().blocks
const currentEdges = get().edges
const filtered = filterNewEdges(edges, currentEdges)
const validEdges = filterValidEdges(edges, blocks)
const filtered = filterNewEdges(validEdges, currentEdges)
const newEdges = [...currentEdges]
for (const edge of filtered) {
@@ -512,7 +549,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
})
}
const blocks = get().blocks
set({
blocks: { ...blocks },
edges: newEdges,
@@ -572,7 +608,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
) => {
set((state) => {
const nextBlocks = workflowState.blocks || {}
const nextEdges = workflowState.edges || []
const nextEdges = filterValidEdges(workflowState.edges || [], nextBlocks)
const nextLoops =
Object.keys(workflowState.loops || {}).length > 0
? workflowState.loops
@@ -1083,7 +1119,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
const newState = {
blocks: deployedState.blocks,
edges: deployedState.edges,
edges: filterValidEdges(deployedState.edges, deployedState.blocks),
loops: deployedState.loops || {},
parallels: deployedState.parallels || {},
needsRedeployment: false,