@marktoflow/gui 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/.turbo/turbo-build.log +26 -0
  2. package/.turbo/turbo-test.log +22 -0
  3. package/README.md +179 -0
  4. package/dist/client/assets/index-DwTI8opO.js +608 -0
  5. package/dist/client/assets/index-DwTI8opO.js.map +1 -0
  6. package/dist/client/assets/index-RoEdL6gO.css +1 -0
  7. package/dist/client/index.html +20 -0
  8. package/dist/client/vite.svg +9 -0
  9. package/dist/server/index.d.ts +3 -0
  10. package/dist/server/index.d.ts.map +1 -0
  11. package/dist/server/index.js +56 -0
  12. package/dist/server/index.js.map +1 -0
  13. package/dist/server/routes/ai.js +50 -0
  14. package/dist/server/routes/ai.js.map +1 -0
  15. package/dist/server/routes/execute.js +62 -0
  16. package/dist/server/routes/execute.js.map +1 -0
  17. package/dist/server/routes/workflows.js +99 -0
  18. package/dist/server/routes/workflows.js.map +1 -0
  19. package/dist/server/server/index.js +95 -0
  20. package/dist/server/server/index.js.map +1 -0
  21. package/dist/server/server/routes/ai.js +87 -0
  22. package/dist/server/server/routes/ai.js.map +1 -0
  23. package/dist/server/server/routes/execute.js +63 -0
  24. package/dist/server/server/routes/execute.js.map +1 -0
  25. package/dist/server/server/routes/tools.js +518 -0
  26. package/dist/server/server/routes/tools.js.map +1 -0
  27. package/dist/server/server/routes/workflows.js +99 -0
  28. package/dist/server/server/routes/workflows.js.map +1 -0
  29. package/dist/server/server/services/AIService.js +69 -0
  30. package/dist/server/server/services/AIService.js.map +1 -0
  31. package/dist/server/server/services/FileWatcher.js +60 -0
  32. package/dist/server/server/services/FileWatcher.js.map +1 -0
  33. package/dist/server/server/services/WorkflowService.js +363 -0
  34. package/dist/server/server/services/WorkflowService.js.map +1 -0
  35. package/dist/server/server/services/agents/claude-code-provider.js +250 -0
  36. package/dist/server/server/services/agents/claude-code-provider.js.map +1 -0
  37. package/dist/server/server/services/agents/claude-provider.js +204 -0
  38. package/dist/server/server/services/agents/claude-provider.js.map +1 -0
  39. package/dist/server/server/services/agents/copilot-provider.js +227 -0
  40. package/dist/server/server/services/agents/copilot-provider.js.map +1 -0
  41. package/dist/server/server/services/agents/demo-provider.js +167 -0
  42. package/dist/server/server/services/agents/demo-provider.js.map +1 -0
  43. package/dist/server/server/services/agents/index.js +31 -0
  44. package/dist/server/server/services/agents/index.js.map +1 -0
  45. package/dist/server/server/services/agents/ollama-provider.js +220 -0
  46. package/dist/server/server/services/agents/ollama-provider.js.map +1 -0
  47. package/dist/server/server/services/agents/prompts.js +436 -0
  48. package/dist/server/server/services/agents/prompts.js.map +1 -0
  49. package/dist/server/server/services/agents/registry.js +242 -0
  50. package/dist/server/server/services/agents/registry.js.map +1 -0
  51. package/dist/server/server/services/agents/types.js +6 -0
  52. package/dist/server/server/services/agents/types.js.map +1 -0
  53. package/dist/server/server/websocket/index.js +85 -0
  54. package/dist/server/server/websocket/index.js.map +1 -0
  55. package/dist/server/services/AIService.d.ts +30 -0
  56. package/dist/server/services/AIService.d.ts.map +1 -0
  57. package/dist/server/services/AIService.js +216 -0
  58. package/dist/server/services/AIService.js.map +1 -0
  59. package/dist/server/services/FileWatcher.d.ts +10 -0
  60. package/dist/server/services/FileWatcher.d.ts.map +1 -0
  61. package/dist/server/services/FileWatcher.js +62 -0
  62. package/dist/server/services/FileWatcher.js.map +1 -0
  63. package/dist/server/services/WorkflowService.d.ts +54 -0
  64. package/dist/server/services/WorkflowService.d.ts.map +1 -0
  65. package/dist/server/services/WorkflowService.js +323 -0
  66. package/dist/server/services/WorkflowService.js.map +1 -0
  67. package/dist/server/shared/constants.js +175 -0
  68. package/dist/server/shared/constants.js.map +1 -0
  69. package/dist/server/shared/types.js +3 -0
  70. package/dist/server/shared/types.js.map +1 -0
  71. package/dist/server/websocket/index.d.ts +10 -0
  72. package/dist/server/websocket/index.d.ts.map +1 -0
  73. package/dist/server/websocket/index.js +85 -0
  74. package/dist/server/websocket/index.js.map +1 -0
  75. package/index.html +19 -0
  76. package/package.json +96 -0
  77. package/playwright.config.ts +27 -0
  78. package/postcss.config.js +6 -0
  79. package/public/vite.svg +9 -0
  80. package/src/client/App.tsx +520 -0
  81. package/src/client/components/Canvas/Canvas.tsx +405 -0
  82. package/src/client/components/Canvas/ExecutionOverlay.tsx +847 -0
  83. package/src/client/components/Canvas/NodeContextMenu.tsx +188 -0
  84. package/src/client/components/Canvas/OutputNode.tsx +111 -0
  85. package/src/client/components/Canvas/StepNode.tsx +106 -0
  86. package/src/client/components/Canvas/SubWorkflowNode.tsx +141 -0
  87. package/src/client/components/Canvas/Toolbar.tsx +189 -0
  88. package/src/client/components/Canvas/TriggerNode.tsx +128 -0
  89. package/src/client/components/Editor/InputsEditor.tsx +458 -0
  90. package/src/client/components/Editor/NewStepWizard.tsx +344 -0
  91. package/src/client/components/Editor/StepEditor.tsx +532 -0
  92. package/src/client/components/Editor/YamlEditor.tsx +160 -0
  93. package/src/client/components/Panels/PropertiesPanel.tsx +589 -0
  94. package/src/client/components/Prompt/ChangePreview.tsx +281 -0
  95. package/src/client/components/Prompt/PromptHistoryPanel.tsx +209 -0
  96. package/src/client/components/Prompt/PromptInput.tsx +108 -0
  97. package/src/client/components/Sidebar/Sidebar.tsx +343 -0
  98. package/src/client/components/common/Breadcrumb.tsx +40 -0
  99. package/src/client/components/common/Button.tsx +68 -0
  100. package/src/client/components/common/ContextMenu.tsx +202 -0
  101. package/src/client/components/common/KeyboardShortcuts.tsx +143 -0
  102. package/src/client/components/common/Modal.tsx +93 -0
  103. package/src/client/components/common/Tabs.tsx +57 -0
  104. package/src/client/components/common/ThemeToggle.tsx +63 -0
  105. package/src/client/components/index.ts +32 -0
  106. package/src/client/hooks/index.ts +4 -0
  107. package/src/client/hooks/useAIPrompt.ts +108 -0
  108. package/src/client/hooks/useCanvas.ts +247 -0
  109. package/src/client/hooks/useWebSocket.ts +164 -0
  110. package/src/client/hooks/useWorkflow.ts +138 -0
  111. package/src/client/main.tsx +10 -0
  112. package/src/client/stores/canvasStore.ts +348 -0
  113. package/src/client/stores/editorStore.ts +133 -0
  114. package/src/client/stores/executionStore.ts +440 -0
  115. package/src/client/stores/index.ts +4 -0
  116. package/src/client/stores/layoutStore.ts +103 -0
  117. package/src/client/stores/navigationStore.ts +49 -0
  118. package/src/client/stores/promptStore.ts +113 -0
  119. package/src/client/stores/themeStore.ts +75 -0
  120. package/src/client/stores/workflowStore.ts +177 -0
  121. package/src/client/styles/globals.css +346 -0
  122. package/src/client/utils/cn.ts +9 -0
  123. package/src/client/utils/index.ts +4 -0
  124. package/src/client/utils/serviceIcons.tsx +64 -0
  125. package/src/client/utils/stepValidation.ts +155 -0
  126. package/src/client/utils/workflowToGraph.ts +299 -0
  127. package/src/server/index.ts +114 -0
  128. package/src/server/routes/ai.ts +91 -0
  129. package/src/server/routes/execute.ts +71 -0
  130. package/src/server/routes/tools.ts +564 -0
  131. package/src/server/routes/workflows.ts +106 -0
  132. package/src/server/services/AIService.ts +105 -0
  133. package/src/server/services/FileWatcher.ts +69 -0
  134. package/src/server/services/WorkflowService.ts +441 -0
  135. package/src/server/services/agents/claude-code-provider.ts +320 -0
  136. package/src/server/services/agents/claude-provider.ts +248 -0
  137. package/src/server/services/agents/copilot-provider.ts +311 -0
  138. package/src/server/services/agents/demo-provider.ts +184 -0
  139. package/src/server/services/agents/index.ts +31 -0
  140. package/src/server/services/agents/ollama-provider.ts +267 -0
  141. package/src/server/services/agents/prompts.ts +482 -0
  142. package/src/server/services/agents/registry.ts +289 -0
  143. package/src/server/services/agents/types.ts +146 -0
  144. package/src/server/websocket/index.ts +104 -0
  145. package/src/shared/constants.ts +180 -0
  146. package/src/shared/types.ts +179 -0
  147. package/tailwind.config.ts +73 -0
  148. package/tests/e2e/app.spec.ts +90 -0
  149. package/tests/e2e/canvas.spec.ts +128 -0
  150. package/tests/e2e/workflow.spec.ts +185 -0
  151. package/tests/integration/api.test.ts +250 -0
  152. package/tests/integration/testApp.ts +31 -0
  153. package/tests/setup.ts +37 -0
  154. package/tests/unit/canvasStore.test.ts +502 -0
  155. package/tests/unit/components.test.tsx +151 -0
  156. package/tests/unit/executionStore.test.ts +527 -0
  157. package/tests/unit/layoutStore.test.ts +194 -0
  158. package/tests/unit/navigationStore.test.ts +152 -0
  159. package/tests/unit/stepValidation.test.ts +226 -0
  160. package/tests/unit/themeStore.test.ts +141 -0
  161. package/tests/unit/workflowToGraph.test.ts +289 -0
  162. package/tsconfig.json +29 -0
  163. package/tsconfig.server.json +28 -0
  164. package/vite.config.ts +31 -0
  165. package/vitest.config.ts +26 -0
@@ -0,0 +1,405 @@
1
+ import { useCallback, useState, useRef, type DragEvent } from 'react';
2
+ import {
3
+ ReactFlow,
4
+ Background,
5
+ Controls,
6
+ MiniMap,
7
+ BackgroundVariant,
8
+ useReactFlow,
9
+ type NodeMouseHandler,
10
+ type Node,
11
+ } from '@xyflow/react';
12
+ import { Edit, Copy, Trash2, Code, Play } from 'lucide-react';
13
+ import { useCanvasStore } from '../../stores/canvasStore';
14
+ import { useWorkflowStore } from '../../stores/workflowStore';
15
+ import { StepNode } from './StepNode';
16
+ import { SubWorkflowNode } from './SubWorkflowNode';
17
+ import { TriggerNode } from './TriggerNode';
18
+ import { OutputNode } from './OutputNode';
19
+ import { StepEditor } from '../Editor/StepEditor';
20
+ import { YamlViewer } from '../Editor/YamlEditor';
21
+ import { Modal } from '../common/Modal';
22
+ import {
23
+ ContextMenu,
24
+ ContextMenuContent,
25
+ ContextMenuItem,
26
+ ContextMenuSeparator,
27
+ ContextMenuShortcut,
28
+ ContextMenuTrigger,
29
+ } from '../common/ContextMenu';
30
+ import { useCanvas } from '../../hooks/useCanvas';
31
+ import { type ToolDefinition } from '../Sidebar/Sidebar';
32
+ import type { WorkflowStep } from '@shared/types';
33
+
34
+ // Custom node types
35
+ const nodeTypes = {
36
+ step: StepNode,
37
+ subworkflow: SubWorkflowNode,
38
+ trigger: TriggerNode,
39
+ output: OutputNode,
40
+ };
41
+
42
+ export function Canvas() {
43
+ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, setNodes } =
44
+ useCanvasStore();
45
+ const { autoLayout, deleteSelected, duplicateSelected } = useCanvas();
46
+ const currentWorkflow = useWorkflowStore((s) => s.currentWorkflow);
47
+ const { screenToFlowPosition } = useReactFlow();
48
+
49
+ // Editor state
50
+ const [editingStep, setEditingStep] = useState<WorkflowStep | null>(null);
51
+ const [isEditorOpen, setIsEditorOpen] = useState(false);
52
+ const [yamlViewStep, setYamlViewStep] = useState<WorkflowStep | null>(null);
53
+ const [isYamlViewOpen, setIsYamlViewOpen] = useState(false);
54
+
55
+ // Context menu state
56
+ const [contextMenuNode, setContextMenuNode] = useState<Node | null>(null);
57
+ const contextMenuRef = useRef<HTMLDivElement>(null);
58
+
59
+ // Handle node double-click to open editor or drill down
60
+ const onNodeDoubleClick: NodeMouseHandler = useCallback(
61
+ (event, node) => {
62
+ event.preventDefault();
63
+
64
+ // Sub-workflow nodes handle their own double-click via the drill-down button
65
+ // Don't open editor for special node types
66
+ if (node.type === 'subworkflow' || node.type === 'trigger' || node.type === 'output') {
67
+ return;
68
+ }
69
+
70
+ const step = currentWorkflow?.steps.find((s) => s.id === node.data.id);
71
+ if (step) {
72
+ setEditingStep(step);
73
+ setIsEditorOpen(true);
74
+ }
75
+ },
76
+ [currentWorkflow]
77
+ );
78
+
79
+ // Get the currently selected step node
80
+ const getSelectedStep = useCallback((): WorkflowStep | null => {
81
+ if (!currentWorkflow) return null;
82
+ const selectedNode = nodes.find((n) => n.selected && n.type === 'step');
83
+ if (!selectedNode) return null;
84
+ return currentWorkflow.steps.find((s) => s.id === selectedNode.data.id) || null;
85
+ }, [currentWorkflow, nodes]);
86
+
87
+ // Get undo/redo and copy/paste functions from canvas store
88
+ const { undo, redo, canUndo, canRedo, copySelected, paste, canPaste } = useCanvasStore();
89
+
90
+ // Handle keyboard shortcuts
91
+ const onKeyDown = useCallback(
92
+ (event: React.KeyboardEvent) => {
93
+ const isMeta = event.metaKey || event.ctrlKey;
94
+
95
+ // Delete selected nodes
96
+ if (event.key === 'Backspace' || event.key === 'Delete') {
97
+ deleteSelected();
98
+ }
99
+ // Duplicate selected nodes
100
+ if (isMeta && event.key === 'd') {
101
+ event.preventDefault();
102
+ duplicateSelected();
103
+ }
104
+ // Auto-layout
105
+ if (isMeta && event.key === 'l') {
106
+ event.preventDefault();
107
+ autoLayout();
108
+ }
109
+ // Undo (Cmd/Ctrl + Z)
110
+ if (isMeta && event.key === 'z' && !event.shiftKey) {
111
+ event.preventDefault();
112
+ if (canUndo()) {
113
+ undo();
114
+ }
115
+ }
116
+ // Redo (Cmd/Ctrl + Shift + Z or Cmd/Ctrl + Y)
117
+ if ((isMeta && event.shiftKey && event.key === 'z') || (isMeta && event.key === 'y')) {
118
+ event.preventDefault();
119
+ if (canRedo()) {
120
+ redo();
121
+ }
122
+ }
123
+ // Copy (Cmd/Ctrl + C)
124
+ if (isMeta && event.key === 'c') {
125
+ event.preventDefault();
126
+ copySelected();
127
+ }
128
+ // Paste (Cmd/Ctrl + V)
129
+ if (isMeta && event.key === 'v') {
130
+ event.preventDefault();
131
+ if (canPaste()) {
132
+ paste();
133
+ }
134
+ }
135
+ // Edit selected step (E key without modifiers)
136
+ if (event.key === 'e' && !isMeta && !event.shiftKey && !event.altKey) {
137
+ event.preventDefault();
138
+ const step = getSelectedStep();
139
+ if (step) {
140
+ setEditingStep(step);
141
+ setIsEditorOpen(true);
142
+ }
143
+ }
144
+ // View YAML (Y key without modifiers)
145
+ if (event.key === 'y' && !isMeta && !event.shiftKey && !event.altKey) {
146
+ event.preventDefault();
147
+ const step = getSelectedStep();
148
+ if (step) {
149
+ setYamlViewStep(step);
150
+ setIsYamlViewOpen(true);
151
+ }
152
+ }
153
+ },
154
+ [deleteSelected, duplicateSelected, autoLayout, getSelectedStep, undo, redo, canUndo, canRedo, copySelected, paste, canPaste]
155
+ );
156
+
157
+ // Handle step save
158
+ const handleStepSave = useCallback(
159
+ (updatedStep: WorkflowStep) => {
160
+ // TODO: Update workflow through store
161
+ console.log('Saving step:', updatedStep);
162
+ setIsEditorOpen(false);
163
+ setEditingStep(null);
164
+ },
165
+ []
166
+ );
167
+
168
+ // Context menu handlers
169
+ const handleContextEdit = useCallback(() => {
170
+ if (!contextMenuNode || !currentWorkflow) return;
171
+ const step = currentWorkflow.steps.find((s) => s.id === contextMenuNode.data.id);
172
+ if (step) {
173
+ setEditingStep(step);
174
+ setIsEditorOpen(true);
175
+ }
176
+ setContextMenuNode(null);
177
+ }, [contextMenuNode, currentWorkflow]);
178
+
179
+ const handleContextViewYaml = useCallback(() => {
180
+ if (!contextMenuNode || !currentWorkflow) return;
181
+ const step = currentWorkflow.steps.find((s) => s.id === contextMenuNode.data.id);
182
+ if (step) {
183
+ setYamlViewStep(step);
184
+ setIsYamlViewOpen(true);
185
+ }
186
+ setContextMenuNode(null);
187
+ }, [contextMenuNode, currentWorkflow]);
188
+
189
+ const handleContextDuplicate = useCallback(() => {
190
+ if (contextMenuNode) {
191
+ // Select the node first, then duplicate
192
+ duplicateSelected();
193
+ }
194
+ setContextMenuNode(null);
195
+ }, [contextMenuNode, duplicateSelected]);
196
+
197
+ const handleContextDelete = useCallback(() => {
198
+ if (contextMenuNode) {
199
+ deleteSelected();
200
+ }
201
+ setContextMenuNode(null);
202
+ }, [contextMenuNode, deleteSelected]);
203
+
204
+ const handleContextExecute = useCallback(() => {
205
+ if (contextMenuNode) {
206
+ console.log('Execute step:', contextMenuNode.data.id);
207
+ // TODO: Implement single step execution
208
+ }
209
+ setContextMenuNode(null);
210
+ }, [contextMenuNode]);
211
+
212
+ // Handle right-click on node
213
+ const onNodeContextMenu = useCallback(
214
+ (event: React.MouseEvent, node: Node) => {
215
+ event.preventDefault();
216
+ // Only show context menu for step nodes
217
+ if (node.type === 'step') {
218
+ setContextMenuNode(node);
219
+ }
220
+ },
221
+ []
222
+ );
223
+
224
+ // Handle drag over for drop target
225
+ const onDragOver = useCallback((event: DragEvent) => {
226
+ event.preventDefault();
227
+ event.dataTransfer.dropEffect = 'copy';
228
+ }, []);
229
+
230
+ // Handle drop from tools palette
231
+ const onDrop = useCallback(
232
+ (event: DragEvent) => {
233
+ event.preventDefault();
234
+
235
+ const toolData = event.dataTransfer.getData('application/marktoflow-tool');
236
+ if (!toolData) return;
237
+
238
+ try {
239
+ const tool: ToolDefinition = JSON.parse(toolData);
240
+
241
+ // Get the position where the node was dropped
242
+ const position = screenToFlowPosition({
243
+ x: event.clientX,
244
+ y: event.clientY,
245
+ });
246
+
247
+ // Create a new node
248
+ const newId = tool.id + '-' + Date.now().toString(36);
249
+ const newNode: Node = {
250
+ id: newId,
251
+ type: 'step',
252
+ position,
253
+ data: {
254
+ id: newId,
255
+ name: tool.name + ' Action',
256
+ action: tool.id + '.' + (tool.actions?.[0] || 'action'),
257
+ status: 'pending',
258
+ },
259
+ };
260
+
261
+ // Add the node to the canvas
262
+ setNodes([...nodes, newNode]);
263
+ } catch (e) {
264
+ console.error('Failed to parse dropped tool data:', e);
265
+ }
266
+ },
267
+ [nodes, setNodes, screenToFlowPosition]
268
+ );
269
+
270
+ // Get available variables for the editing step
271
+ const getAvailableVariables = useCallback((): string[] => {
272
+ if (!currentWorkflow || !editingStep) return [];
273
+
274
+ const variables: string[] = [];
275
+
276
+ // Add input variables
277
+ if (currentWorkflow.inputs) {
278
+ for (const key of Object.keys(currentWorkflow.inputs)) {
279
+ variables.push(`inputs.${key}`);
280
+ }
281
+ }
282
+
283
+ // Add output variables from steps before the editing step
284
+ const stepIndex = currentWorkflow.steps.findIndex(
285
+ (s) => s.id === editingStep.id
286
+ );
287
+ for (let i = 0; i < stepIndex; i++) {
288
+ const step = currentWorkflow.steps[i];
289
+ if (step.outputVariable) {
290
+ variables.push(step.outputVariable);
291
+ }
292
+ }
293
+
294
+ return variables;
295
+ }, [currentWorkflow, editingStep]);
296
+
297
+ return (
298
+ <ContextMenu>
299
+ <ContextMenuTrigger asChild>
300
+ <div
301
+ ref={contextMenuRef}
302
+ className="w-full h-full"
303
+ onKeyDown={onKeyDown}
304
+ onDragOver={onDragOver}
305
+ onDrop={onDrop}
306
+ tabIndex={0}
307
+ >
308
+ <ReactFlow
309
+ nodes={nodes}
310
+ edges={edges}
311
+ onNodesChange={onNodesChange}
312
+ onEdgesChange={onEdgesChange}
313
+ onConnect={onConnect}
314
+ onNodeDoubleClick={onNodeDoubleClick}
315
+ onNodeContextMenu={onNodeContextMenu}
316
+ nodeTypes={nodeTypes}
317
+ fitView
318
+ snapToGrid
319
+ snapGrid={[16, 16]}
320
+ defaultEdgeOptions={{
321
+ type: 'smoothstep',
322
+ animated: true,
323
+ style: { stroke: '#ff6d5a', strokeWidth: 2 },
324
+ }}
325
+ proOptions={{ hideAttribution: true }}
326
+ >
327
+ <Background
328
+ variant={BackgroundVariant.Dots}
329
+ gap={24}
330
+ size={1}
331
+ color="#3d3d5c"
332
+ />
333
+ <Controls />
334
+ <MiniMap
335
+ nodeColor={(node) => {
336
+ switch (node.data?.status) {
337
+ case 'running':
338
+ return '#f0ad4e';
339
+ case 'completed':
340
+ return '#5cb85c';
341
+ case 'failed':
342
+ return '#d9534f';
343
+ default:
344
+ return '#2d2d4a';
345
+ }
346
+ }}
347
+ maskColor="rgba(26, 26, 46, 0.8)"
348
+ />
349
+ </ReactFlow>
350
+ </div>
351
+ </ContextMenuTrigger>
352
+
353
+ {/* Node Context Menu */}
354
+ <ContextMenuContent>
355
+ <ContextMenuItem onClick={handleContextEdit}>
356
+ <Edit className="w-4 h-4 mr-2" />
357
+ Edit Step
358
+ <ContextMenuShortcut>E</ContextMenuShortcut>
359
+ </ContextMenuItem>
360
+ <ContextMenuItem onClick={handleContextViewYaml}>
361
+ <Code className="w-4 h-4 mr-2" />
362
+ View YAML
363
+ <ContextMenuShortcut>Y</ContextMenuShortcut>
364
+ </ContextMenuItem>
365
+ <ContextMenuSeparator />
366
+ <ContextMenuItem onClick={handleContextExecute}>
367
+ <Play className="w-4 h-4 mr-2" />
368
+ Execute Step
369
+ </ContextMenuItem>
370
+ <ContextMenuSeparator />
371
+ <ContextMenuItem onClick={handleContextDuplicate}>
372
+ <Copy className="w-4 h-4 mr-2" />
373
+ Duplicate
374
+ <ContextMenuShortcut>⌘D</ContextMenuShortcut>
375
+ </ContextMenuItem>
376
+ <ContextMenuItem onClick={handleContextDelete} destructive>
377
+ <Trash2 className="w-4 h-4 mr-2" />
378
+ Delete
379
+ <ContextMenuShortcut>⌫</ContextMenuShortcut>
380
+ </ContextMenuItem>
381
+ </ContextMenuContent>
382
+
383
+ {/* Step Editor Modal */}
384
+ <StepEditor
385
+ open={isEditorOpen}
386
+ onOpenChange={setIsEditorOpen}
387
+ step={editingStep}
388
+ onSave={handleStepSave}
389
+ availableVariables={getAvailableVariables()}
390
+ />
391
+
392
+ {/* YAML Viewer Modal */}
393
+ <Modal
394
+ open={isYamlViewOpen}
395
+ onOpenChange={setIsYamlViewOpen}
396
+ title={`YAML: ${yamlViewStep?.name || yamlViewStep?.id}`}
397
+ size="lg"
398
+ >
399
+ <div className="p-4">
400
+ <YamlViewer value={yamlViewStep} />
401
+ </div>
402
+ </Modal>
403
+ </ContextMenu>
404
+ );
405
+ }