Skip to content

Commit e67c356

Browse files
committed
feat(workflow): implement If/Else node with conditional branching and path optimization
- Add IfElseNode component with true/false output paths and property editor - Implement condition evaluation and path selection logic - Move condition state to content property for consistent management - Add skipNodes tracking for inactive conditional paths - Optimize execution logic and reduce code duplication - Update edge type definitions to use ReactFlowEdge - Integrate node into workflow sidebar under Conditionals section This feature enables conditional branching in workflows with improved execution handling and optimized path selection logic.
1 parent 266d26d commit e67c356

File tree

7 files changed

+146
-11
lines changed

7 files changed

+146
-11
lines changed

vscode/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "cody-ai",
44
"private": true,
55
"displayName": "Cody: AI Code Assistant",
6-
"version": "1.67.0+2",
6+
"version": "1.67.0+3",
77
"publisher": "sourcegraph",
88
"license": "Apache-2.0",
99
"icon": "resources/sourcegraph.png",

vscode/src/workflow/workflow-executor.ts

+38
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export async function executeWorkflow(
8181
accumulatorValues: new Map(),
8282
}
8383

84+
const skipNodes = new Set<string>()
85+
8486
// Calculate all inactive nodes
8587
const allInactiveNodes = new Set<string>()
8688
for (const node of nodes) {
@@ -108,6 +110,10 @@ export async function executeWorkflow(
108110
}
109111
}
110112
for (const node of sortedNodes) {
113+
if (skipNodes.has(node.id)) {
114+
continue
115+
}
116+
111117
if (allInactiveNodes.has(node.id)) {
112118
continue
113119
}
@@ -297,6 +303,38 @@ export async function executeWorkflow(
297303
break
298304
}
299305

306+
case NodeType.IF_ELSE: {
307+
const inputs = combineParentOutputsByConnectionOrder(node.id, context)
308+
const condition = node.data.content
309+
? replaceIndexedInputs(node.data.content, inputs, context)
310+
: ''
311+
312+
// Evaluate condition using string comparison
313+
// Format: "${1} === value" or "${1} !== value"
314+
const [leftSide, operator, rightSide] = condition.trim().split(/\s+(===|!==)\s+/)
315+
316+
const hasresult =
317+
operator === '===' ? leftSide === rightSide : leftSide !== rightSide
318+
const resultString = hasresult ? 'true' : 'false'
319+
320+
// Get paths and mark nodes to skip
321+
const edges = context.edgeIndex.bySource.get(node.id) || []
322+
const nonTakenPath = edges.find(
323+
edge => edge.sourceHandle === (hasresult ? 'false' : 'true')
324+
)
325+
if (nonTakenPath) {
326+
const nodesToSkip = getInactiveNodes(edges, nonTakenPath.target)
327+
for (const nodeId of nodesToSkip) {
328+
skipNodes.add(nodeId)
329+
}
330+
}
331+
332+
context.nodeOutputs.set(node.id, resultString)
333+
result = resultString
334+
335+
break
336+
}
337+
300338
default:
301339
persistentShell.dispose()
302340
throw new Error(`Unknown node type: ${(node as WorkflowNodes).type}`)

vscode/webviews/workflow/components/CustomOrderedEdge.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
import { BaseEdge, getSmoothStepPath } from '@xyflow/react'
1+
import { BaseEdge, type Edge as ReactFlowEdge, getSmoothStepPath } from '@xyflow/react'
22
import type { EdgeProps } from '@xyflow/react'
33
import type React from 'react'
44
import { memo } from 'react'
55

6-
export interface Edge {
7-
id: string
8-
source: string
9-
target: string
10-
type?: string
11-
style?: {
12-
strokeWidth: 1
13-
}
14-
}
6+
export type Edge = ReactFlowEdge<any>
157

168
export type OrderedEdgeProps = EdgeProps & {
179
data?: {

vscode/webviews/workflow/components/PropertyEditor.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,19 @@ export const PropertyEditor: React.FC<PropertyEditorProps> = ({ node, onUpdate,
359359
</div>
360360
</div>
361361
)}
362+
{node.type === NodeType.IF_ELSE && (
363+
<div className="tw-flex tw-flex-col tw-gap-2">
364+
<Label htmlFor="node-input">Condition</Label>
365+
<Textarea
366+
id="node-input"
367+
value={node.data.content || ''}
368+
onChange={(e: { target: { value: any } }) =>
369+
onUpdate(node.id, { content: e.target.value })
370+
}
371+
placeholder="Enter input text... (use ${1}, ${2} and so on for positional inputs)"
372+
/>
373+
</div>
374+
)}
362375
</div>
363376
)
364377
}

vscode/webviews/workflow/components/WorkflowSidebar.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,20 @@ export const WorkflowSidebar: React.FC<WorkflowSidebarProps> = ({
153153
</div>
154154
</AccordionContent>
155155
</AccordionItem>
156+
<AccordionItem value="conditionals">
157+
<AccordionTrigger>Conditionals</AccordionTrigger>
158+
<AccordionContent>
159+
<div className="tw-flex tw-flex-col tw-gap-2">
160+
<Button
161+
onClick={() => onNodeAdd('If/Else', NodeType.IF_ELSE)}
162+
className="tw-w-full"
163+
variant="outline"
164+
>
165+
If/Else
166+
</Button>
167+
</div>
168+
</AccordionContent>
169+
</AccordionItem>
156170
<AccordionItem value="context">
157171
<AccordionTrigger>Context Nodes</AccordionTrigger>
158172
<AccordionContent>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Handle, Position } from '@xyflow/react'
2+
import {
3+
type BaseNodeData,
4+
type BaseNodeProps,
5+
NodeType,
6+
type WorkflowNode,
7+
getBorderColor,
8+
getNodeStyle,
9+
} from './Nodes'
10+
11+
export type IfElseNode = Omit<WorkflowNode, 'data'> & {
12+
type: NodeType.IF_ELSE
13+
data: BaseNodeData & {
14+
truePathActive: boolean
15+
falsePathActive: boolean
16+
}
17+
}
18+
19+
export const IfElseNode: React.FC<BaseNodeProps> = ({ data, selected }) => (
20+
<div
21+
style={getNodeStyle(
22+
NodeType.IF_ELSE,
23+
data.moving,
24+
selected,
25+
data.executing,
26+
data.error,
27+
data.active,
28+
data.interrupted
29+
)}
30+
>
31+
<Handle type="target" position={Position.Top} />
32+
<div className="tw-flex tw-flex-col tw-gap-2">
33+
<div
34+
className="tw-text-center tw-py-1 tw-mb-2 tw-rounded-t-sm tw-font-bold"
35+
style={{
36+
background: `linear-gradient(to top, #1e1e1e, ${getBorderColor(NodeType.IF_ELSE, {
37+
error: data.error,
38+
executing: data.executing,
39+
moving: data.moving,
40+
selected,
41+
interrupted: data.interrupted,
42+
active: data.active,
43+
})})`,
44+
color: 'var(--vscode-dropdown-foreground)',
45+
marginLeft: '-0.5rem',
46+
marginRight: '-0.5rem',
47+
marginTop: '-0.5rem',
48+
}}
49+
>
50+
IF...ELSE
51+
</div>
52+
<div className="tw-flex tw-items-center tw-justify-center tw-p-2 tw-border tw-border-[var(--vscode-input-border)] tw-rounded">
53+
<span>{data.condition || 'Condition'}</span>
54+
</div>
55+
<div className="tw-flex tw-justify-between tw-mt-2">
56+
<div className="tw-text-sm tw-opacity-75">True</div>
57+
<div className="tw-text-sm tw-opacity-75">False</div>
58+
</div>
59+
</div>
60+
<Handle type="source" position={Position.Bottom} id="true" style={{ left: '25%' }} />
61+
<Handle type="source" position={Position.Bottom} id="false" style={{ left: '75%' }} />
62+
</div>
63+
)

vscode/webviews/workflow/components/nodes/Nodes.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Edge } from '../CustomOrderedEdge'
55
import { AccumulatorNode } from './Accumulator_Node'
66
import { CLINode } from './CLI_Node'
77
import { CodyOutputNode } from './CodyOutput_Node'
8+
import { IfElseNode } from './IfElse_Node'
89
import { LLMNode } from './LLM_Node'
910
import { LoopEndNode } from './LoopEnd_Node'
1011
import { LoopStartNode } from './LoopStart_Node'
@@ -23,6 +24,7 @@ export enum NodeType {
2324
LOOP_START = 'loop-start',
2425
LOOP_END = 'loop-end',
2526
ACCUMULATOR = 'accumulator',
27+
IF_ELSE = 'if-else',
2628
}
2729

2830
// Shared node props interface
@@ -39,6 +41,7 @@ export interface BaseNodeProps {
3941
iterations?: number
4042
interrupted?: boolean
4143
handlePostMessage: (message: WorkflowToExtension) => void
44+
condition: string
4245
}
4346
selected?: boolean
4447
}
@@ -81,6 +84,7 @@ export type WorkflowNodes =
8184
| LoopStartNode
8285
| LoopEndNode
8386
| AccumulatorNode
87+
| IfElseNode
8488

8589
/**
8690
* Creates a new workflow node with the specified type, label, and position.
@@ -127,6 +131,16 @@ export const createNode = (node: Omit<WorkflowNodes, 'id'>): WorkflowNodes => {
127131
local_remote: false,
128132
},
129133
} as SearchContextNode
134+
case NodeType.IF_ELSE:
135+
return {
136+
...node,
137+
id,
138+
data: {
139+
...node.data,
140+
truePathActive: false,
141+
falsePathActive: false,
142+
},
143+
} as IfElseNode
130144
default:
131145
return {
132146
...node,
@@ -274,4 +288,5 @@ export const nodeTypes = {
274288
[NodeType.LOOP_START]: LoopStartNode,
275289
[NodeType.LOOP_END]: LoopEndNode,
276290
[NodeType.ACCUMULATOR]: AccumulatorNode,
291+
[NodeType.IF_ELSE]: IfElseNode,
277292
}

0 commit comments

Comments
 (0)