Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
86d0330
Fix output node not selectable
j-sanaa Apr 22, 2026
a3c57de
Fix highlight color
j-sanaa Apr 22, 2026
39414ba
Fix previous node displaying label and not ID
j-sanaa Apr 22, 2026
c6fb52e
Fix tool arguements not being called
j-sanaa Apr 22, 2026
08af471
Fix dropdown values to not be bold
j-sanaa Apr 22, 2026
d84a3fd
Add standard sizing for all input text
j-sanaa Apr 23, 2026
36da402
FIx syntax highlighting styling
j-sanaa Apr 23, 2026
be0c3e1
Use predefined tokens for font weight and size
j-sanaa Apr 23, 2026
2c8bc16
Fix dialog size of Source
j-sanaa Apr 23, 2026
21f8200
Fix color of syntax highlight in dark mode
j-sanaa Apr 23, 2026
8dcdeb8
Fix the input fileds color in dark and regular mode
j-sanaa Apr 23, 2026
eb40254
Fix gemini comments
j-sanaa Apr 24, 2026
08441fd
FIx inconsistent spacing
j-sanaa Apr 24, 2026
434508f
Merge branch 'main' into fix/agentflow-node-output
j-sanaa Apr 24, 2026
eb3057c
Standardise gap between label and field
j-sanaa Apr 24, 2026
5db7b2d
Merge branch 'fix/agentflow-node-output' of https://github.com/Flowis…
j-sanaa Apr 24, 2026
ed932f2
Remove export for unused function and standardize dialog size accross…
j-sanaa Apr 24, 2026
edf0ee0
Fix test error
j-sanaa Apr 24, 2026
f4e872a
Fix opening node editing dialog on iteration node
j-sanaa Apr 24, 2026
f6bb880
Untested change
j-sanaa Apr 27, 2026
745d969
Merge branch 'main' into fix-agentflow-iteration-node
j-sanaa Apr 30, 2026
67b2834
Add {{iteration}} variable
j-sanaa Apr 30, 2026
a121895
Add node execution status to Iteration node
j-sanaa Apr 30, 2026
fec0f80
Fix delete iteration node
j-sanaa Apr 30, 2026
328d652
stream iteration node results incrementally when it is the terminal node
j-sanaa Apr 30, 2026
5da7734
Add test cases
j-sanaa Apr 30, 2026
7846497
Fix edit color in dark mode and gemini comments
j-sanaa May 1, 2026
4166d09
Merge branch 'main' into fix-agentflow-iteration-node
j-sanaa May 1, 2026
3139e62
Fix lint warnings
j-sanaa May 1, 2026
e2d509c
Fix review comment - Add iterationEdge z-index value to tokens
j-sanaa May 1, 2026
677a622
keep succesful and error messages in content
j-sanaa May 1, 2026
1b95505
Add removed comment
j-sanaa May 1, 2026
ed80d70
Remove changes to server code in this PR
j-sanaa May 1, 2026
1014f7e
Merge branch 'fix-agentflow-iteration-node' of https://github.com/Flo…
j-sanaa May 1, 2026
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
23 changes: 11 additions & 12 deletions packages/agentflow/src/core/theme/createAgentflowTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ export function createAgentflowTheme(isDarkMode: boolean): Theme {
palette: {
mode,
primary: tokens.colors.palette.primary,
secondary: tokens.colors.palette.secondary,
secondary: {
light: tokens.colors.palette.secondary.light[mode],
main: tokens.colors.palette.secondary.main[mode],
dark: tokens.colors.palette.secondary.dark[mode]
},
success: tokens.colors.palette.success,
error: tokens.colors.palette.error,
warning: tokens.colors.palette.warning,
Expand Down Expand Up @@ -79,21 +83,16 @@ export function createAgentflowTheme(isDarkMode: boolean): Theme {
styleOverrides: {
root: {
'&.Mui-selected': {
color: isDarkMode ? '#fff' : tokens.colors.palette.secondary.dark,
backgroundColor: isDarkMode
? tokens.colors.background.listItemSelected.dark
: tokens.colors.palette.secondary.light,
color: tokens.colors.palette.secondary.dark[mode],
backgroundColor: tokens.colors.background.listItemSelected[mode] || tokens.colors.palette.secondary.light[mode],
'&:hover': {
backgroundColor: isDarkMode
? tokens.colors.background.listItemSelected.dark
: tokens.colors.palette.secondary.light
backgroundColor:
tokens.colors.background.listItemSelected[mode] || tokens.colors.palette.secondary.light[mode]
}
},
'&:hover': {
color: isDarkMode ? '#fff' : tokens.colors.palette.secondary.dark,
backgroundColor: isDarkMode
? tokens.colors.background.listItemSelected.dark
: tokens.colors.palette.secondary.light
color: tokens.colors.palette.secondary.dark[mode],
backgroundColor: tokens.colors.background.listItemSelected[mode] || tokens.colors.palette.secondary.light[mode]
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions packages/agentflow/src/core/theme/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const baseColors = {
secondaryLight: '#ede7f6',
secondaryMain: '#673ab7',
secondaryDark: '#5e35b1',
darkSecondaryLight: '#454c59',
darkSecondaryMain: '#7c4dff',
darkSecondaryDark: '#ffffff',

// MUI palette colors - success (green)
successLight: '#cdf5d8',
Expand Down Expand Up @@ -158,9 +161,9 @@ export const tokens = {
dark: baseColors.primaryDark
},
secondary: {
light: baseColors.secondaryLight,
main: baseColors.secondaryMain,
dark: baseColors.secondaryDark
light: { light: baseColors.secondaryLight, dark: baseColors.darkSecondaryLight },
main: { light: baseColors.secondaryMain, dark: baseColors.darkSecondaryMain },
dark: { light: baseColors.secondaryDark, dark: baseColors.darkSecondaryDark }
},
success: {
light: baseColors.successLight,
Expand Down Expand Up @@ -285,7 +288,11 @@ export const tokens = {
// All values sit below the Canvas Kit modal overlay (30–50).
zIndex: {
canvasButton: 10, // FABs and button containers
canvasPanel: 20 // Open panels/poppers anchored to buttons
canvasPanel: 20, // Open panels/poppers anchored to buttons
// ReactFlow renders group/parent nodes at an elevated stacking context; edges drawn
// between children inside an iteration group must exceed that context to stay visible
// above the group body. 9999 is the conventional ReactFlow ceiling for this use case.
iterationEdge: 9999
}
} as const

Expand Down
1 change: 1 addition & 0 deletions packages/agentflow/src/core/types/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface FlowEdge {
data?: EdgeData
selected?: boolean
animated?: boolean
zIndex?: number
}

export interface FlowData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { NodeInputHandle } from '../components/NodeInputHandle'
import { getMinimumNodeHeight, NodeOutputHandles } from '../components/NodeOutputHandles'
import { NodeStatusIndicator } from '../components/NodeStatusIndicator'
import { NodeToolbarActions } from '../components/NodeToolbarActions'
import { useNodeColors } from '../hooks/useNodeColors'
import { useNodeColors, useOpenNodeEditor } from '../hooks'
import { CardWrapper } from '../styled'

import { NodeInfoDialog } from './NodeInfoDialog'
Expand All @@ -28,7 +28,10 @@ function IterationNodeComponent({ data }: IterationNodeProps) {
const theme = useTheme()
const { isDarkMode } = useConfigContext()
const { apiBaseUrl } = useApiContext()
const { state } = useAgentflowContext()
const { state, executionState } = useAgentflowContext()
const nodeExecution = executionState?.nodeStates[data.id]
const status = nodeExecution?.status ?? data.status
const error = nodeExecution?.error ?? data.error
const ref = useRef<HTMLDivElement>(null)
const reactFlowWrapper = useRef<HTMLDivElement>(null)
const updateNodeInternals = useUpdateNodeInternals()
Expand All @@ -40,6 +43,8 @@ function IterationNodeComponent({ data }: IterationNodeProps) {
height: '250px'
})

const { openNodeEditor } = useOpenNodeEditor()

const { nodeColor, stateColor, backgroundColor } = useNodeColors({
nodeColor: data.color,
selected: data.selected,
Expand Down Expand Up @@ -71,7 +76,7 @@ function IterationNodeComponent({ data }: IterationNodeProps) {
}, [data, ref, updateNodeInternals])

const onResizeEnd = useCallback(
(e: unknown, params: { width: number; height: number }) => {
(_e: unknown, params: { width: number; height: number }) => {
if (!ref.current) return
setCardDimensions({
width: `${params.width}px`,
Expand All @@ -82,7 +87,12 @@ function IterationNodeComponent({ data }: IterationNodeProps) {
)

return (
<div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
<div
ref={ref}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onDoubleClick={() => openNodeEditor(data.id)}
>
<NodeToolbar align='start' isVisible={true}>
<Box style={{ display: 'flex', alignItems: 'center', flexDirection: 'row' }}>
<NodeIcon data={data} apiBaseUrl={apiBaseUrl} />
Expand Down Expand Up @@ -122,7 +132,7 @@ function IterationNodeComponent({ data }: IterationNodeProps) {
}}
border={false}
>
<NodeStatusIndicator status={data.status} error={data.error} />
<NodeStatusIndicator status={status} error={error} />

<Box sx={{ width: '100%' }}>
<NodeInputHandle nodeId={data.id} nodeColor={nodeColor} hidden={data.hideInput} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeFlowEdge, makeFlowNode, makeNodeData } from '@test-utils/factories'
import { act, renderHook } from '@testing-library/react'

import { isValidConnectionAgentflowV2 } from '@/core'
import type { FlowEdge, FlowNode, NodeData } from '@/core/types'
import type { FlowData, FlowEdge, FlowNode, NodeData } from '@/core/types'
import { checkNodePlacementConstraints } from '@/core/validation'

import { useFlowHandlers } from './useFlowHandlers'
Expand Down Expand Up @@ -180,6 +180,44 @@ describe('useFlowHandlers', () => {
expect(onFlowChange).not.toHaveBeenCalled()
expect(mockSetDirty).not.toHaveBeenCalled()
})

it('should set zIndex on edge when both nodes share the same parentNode', () => {
nodes = [makeFlowNode('child_a', { parentNode: 'iter_0' }), makeFlowNode('child_b', { parentNode: 'iter_0' })]
const { result } = renderUseFlowHandlers()

act(() => {
result.current.handleConnect({ source: 'child_a', target: 'child_b', sourceHandle: null, targetHandle: null })
})

expect(onFlowChange).toHaveBeenCalledWith(
expect.objectContaining({
edges: expect.arrayContaining([expect.objectContaining({ zIndex: 9999 })])
})
)
})

it('should not set zIndex when nodes have different parents', () => {
nodes = [makeFlowNode('child_a', { parentNode: 'iter_0' }), makeFlowNode('child_b', { parentNode: 'iter_1' })]
const { result } = renderUseFlowHandlers()

act(() => {
result.current.handleConnect({ source: 'child_a', target: 'child_b', sourceHandle: null, targetHandle: null })
})

const edge = (onFlowChange.mock.calls[0][0] as FlowData).edges[0]
expect(edge.zIndex).toBeUndefined()
})

it('should not set zIndex when nodes are not inside any iteration group', () => {
const { result } = renderUseFlowHandlers()

act(() => {
result.current.handleConnect({ source: 'a', target: 'b', sourceHandle: null, targetHandle: null })
})

const edge = (onFlowChange.mock.calls[0][0] as FlowData).edges[0]
expect(edge.zIndex).toBeUndefined()
})
})

describe('handleNodesChange', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react'
import { addEdge, applyEdgeChanges, applyNodeChanges, Connection, EdgeChange, Node, NodeChange } from 'reactflow'

import { getNodeColor, getUniqueNodeId, getUniqueNodeLabel, initNode, isValidConnectionAgentflowV2, resolveNodeType } from '@/core'
import { tokens } from '@/core/theme/tokens'
import type { FlowDataCallback, FlowEdge, FlowNode, NodeDataSchema } from '@/core/types'
import { checkNodePlacementConstraints } from '@/core/validation'
import { useAgentflowContext } from '@/infrastructure/store'
Expand Down Expand Up @@ -71,9 +72,14 @@ export function useFlowHandlers({
edgeLabel = raw === '0' ? 'proceed' : 'reject'
}

const sourceParent = sourceNode?.parentNode
const targetParent = targetNode?.parentNode
const isWithinIterationNode = sourceParent && targetParent && sourceParent === targetParent

const newEdge = {
...params,
type: 'agentflowEdge',
...(isWithinIterationNode && { zIndex: tokens.zIndex.iterationEdge }),
data: {
sourceColor,
targetColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAvailableVariables } from './useAvailableVariables'
// --- Mocks ---

const mockState = {
nodes: [] as Array<{ id: string; data: Record<string, unknown> }>,
nodes: [] as Array<{ id: string; extent?: string; parentNode?: string; data: Record<string, unknown> }>,
edges: [] as Array<{ source: string; target: string }>
}

Expand Down Expand Up @@ -150,4 +150,47 @@ describe('useAvailableVariables', () => {
const nodeOutputs = result.current.filter((i) => i.category === 'Node Outputs')
expect(nodeOutputs).toHaveLength(0)
})

it('includes $iteration variable when node is inside an iteration group', () => {
mockState.nodes = [
{ id: 'iter_0', data: { id: 'iter_0', name: 'iterationAgentflow', label: 'Iteration' } },
{
id: 'child_0',
extent: 'parent',
parentNode: 'iter_0',
data: { id: 'child_0', name: 'directReplyAgentflow', label: 'Direct Reply' }
}
]

const { result } = renderHook(() => useAvailableVariables('child_0'))

const iterationItems = result.current.filter((i) => i.category === 'Iteration')
expect(iterationItems).toHaveLength(1)
expect(iterationItems[0].label).toBe('$iteration')
expect(iterationItems[0].value).toBe('{{$iteration}}')
})

it('does not include $iteration variable when node is not inside an iteration group', () => {
mockState.nodes = [makeNode('agent_0', 'agentAgentflow')]

const { result } = renderHook(() => useAvailableVariables('agent_0'))

const iterationItems = result.current.filter((i) => i.category === 'Iteration')
expect(iterationItems).toHaveLength(0)
})

it('lists $iteration before other variables when inside an iteration group', () => {
mockState.nodes = [
{
id: 'child_0',
extent: 'parent',
parentNode: 'iter_0',
data: { id: 'child_0', name: 'directReplyAgentflow', label: 'Direct Reply' }
}
]

const { result } = renderHook(() => useAvailableVariables('child_0'))

expect(result.current[0].label).toBe('$iteration')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ export function useAvailableVariables(nodeId: string): VariableItem[] {
return useMemo(() => {
const items: VariableItem[] = [...GLOBAL_VARIABLES]

// Nodes inside an iteration group (extent === 'parent') get access to $iteration
const currentNode = nodes.find((n) => n.id === nodeId)
if (currentNode?.extent === 'parent') {
items.unshift({
label: '$iteration',
description: 'Iteration item. For JSON, use dot notation: $iteration.name',
category: 'Iteration',
value: '{{$iteration}}'
})
}

// ── Upstream node outputs ────────────────────────────────────────
const upstreamNodes = getUpstreamNodes(nodeId, nodes, edges)
for (const node of upstreamNodes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,95 @@ describe('AgentflowContext', () => {
expect(result.current.state.edges).toHaveLength(1)
expect(result.current.state.edges[0].id).toBe('edge-3-4')
})

it('should remove child nodes when an iteration node is deleted', () => {
const initialFlow: FlowData = {
nodes: [
makeFlowNode('iteration-1', { type: 'iteration' }),
makeFlowNode('child-1', { parentNode: 'iteration-1', extent: 'parent' }),
makeFlowNode('child-2', { parentNode: 'iteration-1', extent: 'parent' }),
makeFlowNode('other-1')
],
edges: []
}

const { result } = renderHook(() => useAgentflowContext(), {
wrapper: createWrapper(initialFlow)
})

expect(result.current.state.nodes).toHaveLength(4)

act(() => {
result.current.deleteNode('iteration-1')
})

const remainingIds = result.current.state.nodes.map((n) => n.id)
expect(remainingIds).toEqual(['other-1'])
expect(remainingIds).not.toContain('iteration-1')
expect(remainingIds).not.toContain('child-1')
expect(remainingIds).not.toContain('child-2')
})

it('should remove edges connected to child nodes when an iteration node is deleted', () => {
const initialFlow: FlowData = {
nodes: [
makeFlowNode('iteration-1', { type: 'iteration' }),
makeFlowNode('child-1', { parentNode: 'iteration-1', extent: 'parent' }),
makeFlowNode('child-2', { parentNode: 'iteration-1', extent: 'parent' }),
makeFlowNode('upstream-1'),
makeFlowNode('downstream-1')
],
edges: [
makeEdge('upstream-1', 'iteration-1', { id: 'edge-in' }),
makeEdge('child-1', 'child-2', { id: 'edge-inner' }),
makeEdge('iteration-1', 'downstream-1', { id: 'edge-out' }),
makeEdge('upstream-1', 'downstream-1', { id: 'edge-unrelated' })
]
}

const { result } = renderHook(() => useAgentflowContext(), {
wrapper: createWrapper(initialFlow)
})

expect(result.current.state.edges).toHaveLength(4)

act(() => {
result.current.deleteNode('iteration-1')
})

const remainingEdgeIds = result.current.state.edges.map((e) => e.id)
expect(remainingEdgeIds).toEqual(['edge-unrelated'])
expect(remainingEdgeIds).not.toContain('edge-in')
expect(remainingEdgeIds).not.toContain('edge-inner')
expect(remainingEdgeIds).not.toContain('edge-out')
})

it('should recursively remove nested children when an iteration node is deleted', () => {
const initialFlow: FlowData = {
nodes: [
makeFlowNode('iteration-1', { type: 'iteration' }),
makeFlowNode('child-1', { parentNode: 'iteration-1', extent: 'parent' }),
makeFlowNode('iteration-2', { parentNode: 'iteration-1', extent: 'parent', type: 'iteration' }),
makeFlowNode('grandchild-1', { parentNode: 'iteration-2', extent: 'parent' }),
makeFlowNode('other-1')
],
edges: [makeEdge('grandchild-1', 'child-1', { id: 'edge-grand' })]
}

const { result } = renderHook(() => useAgentflowContext(), {
wrapper: createWrapper(initialFlow)
})

expect(result.current.state.nodes).toHaveLength(5)

act(() => {
result.current.deleteNode('iteration-1')
})

const remainingIds = result.current.state.nodes.map((n) => n.id)
expect(remainingIds).toEqual(['other-1'])
expect(result.current.state.edges).toHaveLength(0)
})
})

describe('duplicateNode', () => {
Expand Down
Loading
Loading