Skip to content

Commit 4fe511e

Browse files
committed
feat(workflow): implement variable nodes and enhance workflow execution
- Add Variable node type with UI representation and configuration - Implement variable value storage and processing in workflow context - Clear preview node content before execution - Add variable placeholder replacement (${variableName}) in inputs - Improve loader icon visibility in right sidebar - Remove content field from AccumulatorNode - Bump version to 1.69.0+1
1 parent a5643b2 commit 4fe511e

9 files changed

+169
-27
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.69.0+0",
6+
"version": "1.69.0+1",
77
"publisher": "sourcegraph",
88
"license": "Apache-2.0",
99
"icon": "resources/sourcegraph.png",

vscode/src/workflow/workflow-executor.ts

+50-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type WorkflowNode,
2626
type WorkflowNodes,
2727
} from '../../webviews/workflow/components/nodes/Nodes'
28+
import type { VariableNode } from '../../webviews/workflow/components/nodes/Variable_Node'
2829
import type { ExtensionToWorkflow } from '../../webviews/workflow/services/WorkflowProtocol'
2930
import { ChatController, type ChatSession } from '../chat/chat-view/ChatController'
3031
import { type ContextRetriever, toStructuredMentions } from '../chat/chat-view/ContextRetriever'
@@ -59,6 +60,8 @@ export interface IndexedExecutionContext {
5960
exitCode: string
6061
}
6162
>
63+
variableValues?: Map<string, string>
64+
ifelseSkipPaths?: Map<string, Set<string>>
6265
}
6366

6467
/**
@@ -91,10 +94,10 @@ export async function executeWorkflow(
9194
loopStates: new Map(),
9295
accumulatorValues: new Map(),
9396
cliMetadata: new Map(),
97+
variableValues: new Map(),
98+
ifelseSkipPaths: new Map(),
9499
}
95100

96-
const skipNodes = new Set<string>()
97-
98101
// Calculate all inactive nodes
99102
const allInactiveNodes = new Set<string>()
100103
for (const node of nodes) {
@@ -113,7 +116,11 @@ export async function executeWorkflow(
113116
} as ExtensionToWorkflow)
114117

115118
for (const node of sortedNodes) {
116-
if (skipNodes.has(node.id)) {
119+
const shouldSkip = Array.from(context.ifelseSkipPaths?.values() ?? []).some(skipNodes =>
120+
skipNodes.has(node.id)
121+
)
122+
123+
if (shouldSkip) {
117124
continue
118125
}
119126

@@ -293,7 +300,22 @@ export async function executeWorkflow(
293300
}
294301

295302
case NodeType.IF_ELSE: {
296-
result = await executeIfElseNode(context, node, skipNodes)
303+
result = await executeIfElseNode(context, node)
304+
break
305+
}
306+
case NodeType.VARIABLE: {
307+
const inputs = combineParentOutputsByConnectionOrder(node.id, context)
308+
const inputValue = node.data.content
309+
? replaceIndexedInputs(node.data.content, inputs, context)
310+
: ''
311+
const {
312+
data: { variableName, initialValue },
313+
} = node as VariableNode
314+
let variableValue = context.variableValues?.get(variableName) || initialValue || ''
315+
variableValue = inputValue
316+
context.variableValues?.set(variableName, variableValue)
317+
318+
result = variableValue
297319
break
298320
}
299321

@@ -414,6 +436,15 @@ export function replaceIndexedInputs(
414436
context.accumulatorValues?.get(varName) || ''
415437
)
416438
}
439+
440+
// Only replace variable variables that are explicitly defined
441+
const variableVars = context.variableValues ? Array.from(context.variableValues.keys()) : []
442+
for (const varName of variableVars) {
443+
result = result.replace(
444+
new RegExp(`\\$\{${varName}}(?!\\w)`, 'g'),
445+
context.variableValues?.get(varName) || ''
446+
)
447+
}
417448
}
418449

419450
return result
@@ -758,8 +789,7 @@ async function executeCodyOutputNode(
758789

759790
async function executeIfElseNode(
760791
context: IndexedExecutionContext,
761-
node: WorkflowNode | IfElseNode,
762-
skipNodes: Set<string>
792+
node: WorkflowNode | IfElseNode
763793
): Promise<string> {
764794
let result = ''
765795
const parentEdges = context.edgeIndex.byTarget.get(node.id) || []
@@ -793,10 +823,23 @@ async function executeIfElseNode(
793823
}
794824

795825
// Get paths and mark nodes to skip
826+
context.ifelseSkipPaths?.set(node.id, new Set<string>())
796827
const edges = context.edgeIndex.bySource.get(node.id) || []
797828
const nonTakenPath = edges.find(edge => edge.sourceHandle === (hasResult ? 'false' : 'true'))
798829
if (nonTakenPath) {
799-
const nodesToSkip = getInactiveNodes(edges, nonTakenPath.target)
830+
// Initialize ifelseSkipPaths if it's undefined
831+
if (!context.ifelseSkipPaths) {
832+
context.ifelseSkipPaths = new Map<string, Set<string>>()
833+
}
834+
835+
// Get or create the set of nodes to skip for this IfElse node
836+
let skipNodes = context.ifelseSkipPaths?.get(node.id)
837+
838+
skipNodes = new Set<string>()
839+
context.ifelseSkipPaths?.set(node.id, skipNodes)
840+
841+
const allEdges = Array.from(context.edgeIndex.byId.values())
842+
const nodesToSkip = getInactiveNodes(allEdges, nonTakenPath.target)
800843
for (const nodeId of nodesToSkip) {
801844
skipNodes.add(nodeId)
802845
}

vscode/webviews/workflow/components/PropertyEditor.tsx

+28-14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { LLMNode } from './nodes/LLM_Node'
2121
import type { LoopStartNode } from './nodes/LoopStart_Node'
2222
import { NodeType, type WorkflowNodes } from './nodes/Nodes'
2323
import type { SearchContextNode } from './nodes/SearchContext_Node'
24+
import type { VariableNode } from './nodes/Variable_Node'
2425

2526
interface PropertyEditorProps {
2627
node: WorkflowNodes
@@ -340,20 +341,6 @@ export const PropertyEditor: React.FC<PropertyEditorProps> = ({ node, onUpdate,
340341
required // Make it required for clarity
341342
/>
342343
</div>
343-
{/* Future: Accumulation Type Dropdown */}
344-
{/* <div>
345-
<Label htmlFor="accumulator-type">Accumulation Type (Future)</Label>
346-
<Select>
347-
<SelectTrigger className="tw-w-full">
348-
<SelectValue placeholder="Concatenate (Default)" />
349-
</SelectTrigger>
350-
<SelectContent>
351-
<SelectItem value="concatenate">Concatenate</SelectItem>
352-
<SelectItem value="sum">Sum (Numbers)</SelectItem>
353-
<SelectItem value="custom">Custom (Future)</SelectItem>
354-
</SelectContent>
355-
</Select>
356-
</div> */}
357344
<div>
358345
<Label htmlFor="accumulator-initial-value">Input Text</Label>
359346
<Textarea
@@ -380,6 +367,33 @@ export const PropertyEditor: React.FC<PropertyEditorProps> = ({ node, onUpdate,
380367
/>
381368
</div>
382369
)}
370+
{node.type === NodeType.VARIABLE && (
371+
<div className="tw-flex tw-flex-col tw-gap-4">
372+
<div>
373+
<Label htmlFor="variable-name">Variable Name</Label>
374+
<Input
375+
id="variable-name"
376+
value={(node as VariableNode).data.variableName || ''}
377+
onChange={(e: { target: { value: any } }) =>
378+
onUpdate(node.id, { variableName: e.target.value })
379+
}
380+
placeholder="Unique variable name to access variable value (e.g., userInput)"
381+
required // Make it required for clarity
382+
/>
383+
</div>
384+
<div>
385+
<Label htmlFor="variable-initial-value">Initial Value</Label>
386+
<Textarea
387+
id="node-input"
388+
value={node.data.content || ''}
389+
onChange={(e: { target: { value: any } }) =>
390+
onUpdate(node.id, { content: e.target.value })
391+
}
392+
placeholder="Enter input text... (use ${1}, ${2} and so on for positional inputs)"
393+
/>
394+
</div>
395+
</div>
396+
)}
383397
</div>
384398
)
385399
}

vscode/webviews/workflow/components/RightSidebar.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({
103103
<div className="tw-w-4 tw-mr-2">
104104
{node.id === executingNodeId && (
105105
<Loader2Icon
106-
strokeWidth={1.5}
107-
size={16}
106+
stroke="#33ffcc"
107+
strokeWidth={3}
108+
size={24}
108109
className="tw-h-4 tw-w-4 tw-animate-spin"
109110
/>
110111
)}

vscode/webviews/workflow/components/WorkflowSidebar.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ export const WorkflowSidebar: React.FC<WorkflowSidebarProps> = ({
150150
>
151151
Accumulator
152152
</Button>
153+
<Button
154+
onClick={() => onNodeAdd('Variable', NodeType.VARIABLE)} // ADD VARIABLE BUTTON
155+
className="tw-w-full"
156+
variant="outline"
157+
>
158+
Variable
159+
</Button>
153160
</div>
154161
</AccordionContent>
155162
</AccordionItem>

vscode/webviews/workflow/components/hooks/workflowExecution.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ export const useWorkflowExecution = (
5959
return
6060
}
6161

62+
const updatedNodes = nodes.map(node => {
63+
if (node.type === NodeType.PREVIEW) {
64+
return {
65+
...node,
66+
data: {
67+
...node.data,
68+
content: '',
69+
tokenCount: 0,
70+
},
71+
}
72+
}
73+
return node
74+
})
75+
setNodes(updatedNodes)
76+
setNodeResults(new Map())
77+
6278
setNodeErrors(new Map())
6379
const controller = new AbortController()
6480
setAbortController(controller)
@@ -67,9 +83,9 @@ export const useWorkflowExecution = (
6783

6884
vscodeAPI.postMessage({
6985
type: 'execute_workflow',
70-
data: { nodes, edges },
86+
data: { nodes: updatedNodes, edges },
7187
})
72-
}, [nodes, edges, vscodeAPI])
88+
}, [nodes, edges, setNodes, vscodeAPI])
7389

7490
const onAbort = useCallback(() => {
7591
if (abortController) {

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

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export type AccumulatorNode = Omit<WorkflowNode, 'data'> & {
1313
data: BaseNodeData & {
1414
variableName: string
1515
initialValue?: string
16-
content?: string
1716
}
1817
}
1918

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

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LoopStartNode } from './LoopStart_Node'
1212
import { PreviewNode } from './Preview_Node'
1313
import { SearchContextNode } from './SearchContext_Node'
1414
import { TextNode } from './Text_Node'
15+
import { VariableNode } from './Variable_Node'
1516

1617
// Core type definitions
1718
export enum NodeType {
@@ -24,6 +25,7 @@ export enum NodeType {
2425
LOOP_START = 'loop-start',
2526
LOOP_END = 'loop-end',
2627
ACCUMULATOR = 'accumulator',
28+
VARIABLE = 'variable',
2729
IF_ELSE = 'if-else',
2830
}
2931

@@ -84,6 +86,7 @@ export type WorkflowNodes =
8486
| LoopStartNode
8587
| LoopEndNode
8688
| AccumulatorNode
89+
| VariableNode
8790
| IfElseNode
8891

8992
/**
@@ -298,4 +301,5 @@ export const nodeTypes = {
298301
[NodeType.LOOP_END]: LoopEndNode,
299302
[NodeType.ACCUMULATOR]: AccumulatorNode,
300303
[NodeType.IF_ELSE]: IfElseNode,
304+
[NodeType.VARIABLE]: VariableNode,
301305
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 VariableNode = Omit<WorkflowNode, 'data'> & {
12+
type: NodeType.VARIABLE
13+
data: BaseNodeData & {
14+
variableName: string
15+
initialValue?: string
16+
}
17+
}
18+
19+
export const VariableNode: React.FC<BaseNodeProps> = ({ data, selected }) => (
20+
<div
21+
style={getNodeStyle(
22+
NodeType.INPUT,
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">
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.INPUT, {
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: '#1e1e1e',
45+
marginLeft: '-0.5rem',
46+
marginRight: '-0.5rem',
47+
marginTop: '-0.5rem',
48+
}}
49+
>
50+
VARIABLE
51+
</div>
52+
<div className="tw-flex tw-items-center tw-justify-center">
53+
<span>{data.title}</span>
54+
</div>
55+
</div>
56+
<Handle type="source" position={Position.Bottom} />
57+
</div>
58+
)

0 commit comments

Comments
 (0)