Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,47 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children }) => <p className='mb-0 text-[#E5E5E5] text-sm'>{children}</p>,
p: ({ children }) => (
<p className='!mt-0 !-mb-4 text-[#E5E5E5] text-sm leading-tight'>{children}</p>
),
h1: ({ children }) => (
<h1 className='mt-0 mb-[-2px] font-semibold text-[#E5E5E5] text-lg'>{children}</h1>
<h1 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-lg leading-tight'>
{children}
</h1>
),
h2: ({ children }) => (
<h2 className='mt-0 mb-[-2px] font-semibold text-[#E5E5E5] text-base'>{children}</h2>
<h2 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-base leading-tight'>
{children}
</h2>
),
h3: ({ children }) => (
<h3 className='mt-0 mb-[-2px] font-semibold text-[#E5E5E5] text-sm'>{children}</h3>
<h3 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-sm leading-tight'>
{children}
</h3>
),
h4: ({ children }) => (
<h4 className='mt-0 mb-[-2px] font-semibold text-[#E5E5E5] text-xs'>{children}</h4>
<h4 className='!mt-0 !-mb-4 font-semibold text-[#E5E5E5] text-xs leading-tight'>
{children}
</h4>
),
ul: ({ children }) => (
<ul className='-mt-[2px] mb-0 list-disc pl-4 text-[#E5E5E5] text-sm'>{children}</ul>
<ul className='!-mt-4 !-mb-4 [&_li>ul]:!mt-0 [&_li>ul]:!mb-0 [&_li>ol]:!mt-0 [&_li>ol]:!mb-0 list-disc pl-4 text-[#E5E5E5] text-sm leading-tight'>
{children}
</ul>
),
ol: ({ children }) => (
<ol className='-mt-[2px] mb-0 list-decimal pl-4 text-[#E5E5E5] text-sm'>{children}</ol>
<ol className='!-mt-4 !-mb-4 [&_li>ul]:!mt-0 [&_li>ul]:!mb-0 [&_li>ol]:!mt-0 [&_li>ol]:!mb-0 list-decimal pl-4 text-[#E5E5E5] text-sm leading-tight'>
{children}
</ol>
),
li: ({ children }) => <li className='mb-0'>{children}</li>,
li: ({ children }) => <li className='!mb-0 leading-tight'>{children}</li>,
code: ({ inline, children }: any) =>
inline ? (
<code className='rounded bg-[var(--divider)] px-1 py-0.5 text-[#F59E0B] text-xs'>
<code className='break-words rounded bg-[var(--divider)] px-1 py-0.5 text-[#F59E0B] text-xs'>
{children}
</code>
) : (
<code className='block rounded bg-[#1A1A1A] p-2 text-[#E5E5E5] text-xs'>
<code className='block whitespace-pre-wrap break-words rounded bg-[#1A1A1A] p-2 text-[#E5E5E5] text-xs'>
{children}
</code>
),
Expand All @@ -78,7 +92,7 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
strong: ({ children }) => <strong className='font-semibold text-white'>{children}</strong>,
em: ({ children }) => <em className='text-[#B8B8B8]'>{children}</em>,
blockquote: ({ children }) => (
<blockquote className='m-0 border-[#F59E0B] border-l-2 pl-3 text-[#B8B8B8] italic'>
<blockquote className='!mt-0 !-mb-4 border-[#F59E0B] border-l-2 pl-3 text-[#B8B8B8] italic'>
{children}
</blockquote>
),
Expand Down Expand Up @@ -187,9 +201,7 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
) : showMarkdown ? (
<NoteMarkdown content={content} />
) : (
<p className='whitespace-pre-wrap text-[#E5E5E5] text-sm leading-relaxed'>
{content}
</p>
<p className='whitespace-pre-wrap text-[#E5E5E5] text-sm leading-snug'>{content}</p>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useCallback, useEffect, useState } from 'react'
import { shallow } from 'zustand/shallow'
import { createLogger } from '@/lib/logs/console/logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
Expand Down Expand Up @@ -109,7 +110,11 @@ export function useMentionData(props: UseMentionDataProps) {
const [workflowBlocks, setWorkflowBlocks] = useState<WorkflowBlockItem[]>([])
const [isLoadingWorkflowBlocks, setIsLoadingWorkflowBlocks] = useState(false)

const workflowStoreBlocks = useWorkflowStore((state) => state.blocks)
// Only subscribe to block keys to avoid re-rendering on position updates
const blockKeys = useWorkflowStore(
useCallback((state) => Object.keys(state.blocks), []),
shallow
)

// Use workflow registry as source of truth for workflows
const registryWorkflows = useWorkflowRegistry((state) => state.workflows)
Expand Down Expand Up @@ -139,15 +144,19 @@ export function useMentionData(props: UseMentionDataProps) {

/**
* Syncs workflow blocks from store
* Only re-runs when blocks are added/removed (not on position updates)
*/
useEffect(() => {
const syncWorkflowBlocks = async () => {
if (!workflowId || !workflowStoreBlocks || Object.keys(workflowStoreBlocks).length === 0) {
if (!workflowId || blockKeys.length === 0) {
setWorkflowBlocks([])
return
}

try {
// Fetch current blocks from store
const workflowStoreBlocks = useWorkflowStore.getState().blocks

const { registry: blockRegistry } = await import('@/blocks/registry')
const mapped = Object.values(workflowStoreBlocks).map((b: any) => {
const reg = (blockRegistry as any)[b.type]
Expand All @@ -169,7 +178,7 @@ export function useMentionData(props: UseMentionDataProps) {
}

syncWorkflowBlocks()
}, [workflowStoreBlocks, workflowId])
}, [blockKeys, workflowId])

/**
* Ensures past chats are loaded
Expand Down Expand Up @@ -323,10 +332,10 @@ export function useMentionData(props: UseMentionDataProps) {
if (!workflowId) return
logger.debug('ensureWorkflowBlocksLoaded called', {
workflowId,
storeBlocksCount: Object.keys(workflowStoreBlocks || {}).length,
storeBlocksCount: blockKeys.length,
workflowBlocksCount: workflowBlocks.length,
})
}, [workflowId, workflowStoreBlocks, workflowBlocks.length])
}, [workflowId, blockKeys.length, workflowBlocks.length])

return {
// State
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,14 @@ export function Editor() {
/>
) : (
<h2
className='min-w-0 flex-1 cursor-pointer truncate pr-[8px] font-medium text-[14px] text-[var(--white)] dark:text-[var(--white)]'
className='min-w-0 flex-1 cursor-pointer select-none truncate pr-[8px] font-medium text-[14px] text-[var(--white)] dark:text-[var(--white)]'
title={title}
onDoubleClick={handleStartRename}
onMouseDown={(e) => {
if (e.detail === 2) {
e.preventDefault()
}
}}
>
{title}
</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DEFAULT_CONTAINER_HEIGHT = 300
* Hook providing utilities for node position, hierarchy, and dimension calculations
*/
export function useNodeUtilities(blocks: Record<string, any>) {
const { getNodes, project } = useReactFlow()
const { getNodes } = useReactFlow()

/**
* Check if a block is a container type (loop, parallel, or subflow)
Expand Down
78 changes: 47 additions & 31 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const WorkflowContent = React.memo(() => {
// Hooks
const params = useParams()
const router = useRouter()
const { project, getNodes, fitView } = useReactFlow()
const { screenToFlowPosition, getNodes, fitView } = useReactFlow()
const { emitCursorUpdate } = useSocket()

// Get workspace ID from the params
Expand Down Expand Up @@ -170,7 +170,7 @@ const WorkflowContent = React.memo(() => {
// Get diff analysis for edge reconstruction
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore()

// Reconstruct deleted edges when viewing original workflow and filter trigger edges
// Reconstruct deleted edges when viewing original workflow and filter out invalid edges
const edgesForDisplay = useMemo(() => {
let edgesToFilter = edges

Expand Down Expand Up @@ -237,7 +237,21 @@ const WorkflowContent = React.memo(() => {
// Combine existing edges with reconstructed deleted edges
edgesToFilter = [...edges, ...reconstructedEdges]
}
return edgesToFilter

// Filter out edges that connect to/from annotation-only blocks (note blocks)
// These blocks don't have handles and shouldn't have connections
return edgesToFilter.filter((edge) => {
const sourceBlock = blocks[edge.source]
const targetBlock = blocks[edge.target]

// Remove edge if either source or target is an annotation-only block
if (!sourceBlock || !targetBlock) return false
if (isAnnotationOnlyBlock(sourceBlock.type) || isAnnotationOnlyBlock(targetBlock.type)) {
return false
}

return true
})
}, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks])

// User permissions - get current user's specific permissions from context
Expand Down Expand Up @@ -680,7 +694,11 @@ const WorkflowContent = React.memo(() => {
// Auto-connect logic for blocks inside containers
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
let autoConnectEdge
if (isAutoConnectEnabled && data.type !== 'starter') {
if (
isAutoConnectEnabled &&
data.type !== 'starter' &&
!isAnnotationOnlyBlock(data.type)
) {
if (existingChildBlocks.length > 0) {
// Connect to the nearest existing child block within the container
const closestBlock = existingChildBlocks
Expand All @@ -694,7 +712,7 @@ const WorkflowContent = React.memo(() => {
.sort((a, b) => a.distance - b.distance)[0]?.block

if (closestBlock) {
// Don't create edges into trigger blocks
// Don't create edges into trigger blocks or annotation blocks
const targetBlockConfig = getBlock(data.type)
const isTargetTrigger =
data.enableTriggerMode === true || targetBlockConfig?.category === 'triggers'
Expand Down Expand Up @@ -769,10 +787,14 @@ const WorkflowContent = React.memo(() => {
// Regular auto-connect logic
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
let autoConnectEdge
if (isAutoConnectEnabled && data.type !== 'starter') {
if (
isAutoConnectEnabled &&
data.type !== 'starter' &&
!isAnnotationOnlyBlock(data.type)
) {
const closestBlock = findClosestOutput(position)
if (closestBlock) {
// Don't create edges into trigger blocks
// Don't create edges into trigger blocks or annotation blocks
const targetBlockConfig = getBlock(data.type)
const isTargetTrigger =
data.enableTriggerMode === true || targetBlockConfig?.category === 'triggers'
Expand Down Expand Up @@ -842,7 +864,7 @@ const WorkflowContent = React.memo(() => {
const baseName = type === 'loop' ? 'Loop' : 'Parallel'
const name = getUniqueBlockName(baseName, blocks)

const centerPosition = project({
const centerPosition = screenToFlowPosition({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
})
Expand Down Expand Up @@ -891,7 +913,7 @@ const WorkflowContent = React.memo(() => {
}

// Calculate the center position of the viewport
const centerPosition = project({
const centerPosition = screenToFlowPosition({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
})
Expand All @@ -906,11 +928,11 @@ const WorkflowContent = React.memo(() => {
// Auto-connect logic
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
let autoConnectEdge
if (isAutoConnectEnabled && type !== 'starter') {
if (isAutoConnectEnabled && type !== 'starter' && !isAnnotationOnlyBlock(type)) {
const closestBlock = findClosestOutput(centerPosition)
logger.info('Closest block found:', closestBlock)
if (closestBlock) {
// Don't create edges into trigger blocks
// Don't create edges into trigger blocks or annotation blocks
const targetBlockConfig = blockConfig
const isTargetTrigger = enableTriggerMode || targetBlockConfig?.category === 'triggers'

Expand Down Expand Up @@ -977,7 +999,7 @@ const WorkflowContent = React.memo(() => {
)
}
}, [
project,
screenToFlowPosition,
blocks,
addBlock,
addEdge,
Expand Down Expand Up @@ -1014,7 +1036,7 @@ const WorkflowContent = React.memo(() => {
}

const bounds = canvasElement.getBoundingClientRect()
const position = project({
const position = screenToFlowPosition({
x: detail.clientX - bounds.left,
y: detail.clientY - bounds.top,
})
Expand All @@ -1041,7 +1063,7 @@ const WorkflowContent = React.memo(() => {
'toolbar-drop-on-empty-workflow-overlay',
handleOverlayToolbarDrop as EventListener
)
}, [project, handleToolbarDrop])
}, [screenToFlowPosition, handleToolbarDrop])

/**
* Recenter canvas when diff appears
Expand Down Expand Up @@ -1090,7 +1112,7 @@ const WorkflowContent = React.memo(() => {
if (!data?.type) return

const reactFlowBounds = event.currentTarget.getBoundingClientRect()
const position = project({
const position = screenToFlowPosition({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
})
Expand All @@ -1106,22 +1128,22 @@ const WorkflowContent = React.memo(() => {
logger.error('Error dropping block on ReactFlow canvas:', { err })
}
},
[project, handleToolbarDrop]
[screenToFlowPosition, handleToolbarDrop]
)

const handleCanvasPointerMove = useCallback(
(event: React.PointerEvent<Element>) => {
const target = event.currentTarget as HTMLElement
const bounds = target.getBoundingClientRect()

const position = project({
const position = screenToFlowPosition({
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
})

emitCursorUpdate(position)
},
[project, emitCursorUpdate]
[screenToFlowPosition, emitCursorUpdate]
)

const handleCanvasPointerLeave = useCallback(() => {
Expand All @@ -1144,7 +1166,7 @@ const WorkflowContent = React.memo(() => {

try {
const reactFlowBounds = event.currentTarget.getBoundingClientRect()
const position = project({
const position = screenToFlowPosition({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
})
Expand Down Expand Up @@ -1188,7 +1210,7 @@ const WorkflowContent = React.memo(() => {
logger.error('Error in onDragOver', { err })
}
},
[project, isPointInLoopNode, getNodes]
[screenToFlowPosition, isPointInLoopNode, getNodes]
)

// Initialize workflow when it exists in registry and isn't active
Expand Down Expand Up @@ -1584,8 +1606,8 @@ const WorkflowContent = React.memo(() => {
// Store currently dragged node ID
setDraggedNodeId(node.id)

// Emit collaborative position update during drag for smooth real-time movement
collaborativeUpdateBlockPosition(node.id, node.position, false)
// Note: We don't emit position updates during drag to avoid flooding socket events.
// The final position is sent in onNodeDragStop for collaborative updates.

// Get the current parent ID of the node being dragged
const currentParentId = blocks[node.id]?.data?.parentId || null
Expand Down Expand Up @@ -1721,14 +1743,7 @@ const WorkflowContent = React.memo(() => {
}
}
},
[
getNodes,
potentialParentId,
blocks,
getNodeAbsolutePosition,
getNodeDepth,
collaborativeUpdateBlockPosition,
]
[getNodes, potentialParentId, blocks, getNodeAbsolutePosition, getNodeDepth]
)

// Add in a nodeDrag start event to set the dragStartParentId
Expand Down Expand Up @@ -1855,7 +1870,8 @@ const WorkflowContent = React.memo(() => {

// Auto-connect when moving an existing block into a container
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
if (isAutoConnectEnabled) {
// Don't auto-connect annotation blocks (like note blocks)
if (isAutoConnectEnabled && !isAnnotationOnlyBlock(node.data?.type)) {
// Existing children in the target container (excluding the moved node)
const existingChildBlocks = Object.values(blocks).filter(
(b) => b.data?.parentId === potentialParentId && b.id !== node.id
Expand Down