@marktoflow/gui 2.0.0-alpha.3 → 2.0.0-alpha.5

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 (95) hide show
  1. package/.turbo/turbo-build.log +24 -8
  2. package/README.md +11 -1
  3. package/dist/client/assets/index-CM44OayM.js +704 -0
  4. package/dist/client/assets/index-CM44OayM.js.map +1 -0
  5. package/dist/client/assets/index-Dru63gi6.css +1 -0
  6. package/dist/client/index.html +2 -2
  7. package/dist/server/{server/index.js → index.js} +22 -1
  8. package/dist/server/index.js.map +1 -0
  9. package/dist/server/routes/executions.js +125 -0
  10. package/dist/server/routes/executions.js.map +1 -0
  11. package/dist/server/{server/routes → routes}/workflows.js +37 -1
  12. package/dist/server/routes/workflows.js.map +1 -0
  13. package/dist/server/{server/services → services}/WorkflowService.js +158 -15
  14. package/dist/server/services/WorkflowService.js.map +1 -0
  15. package/dist/server/{server/websocket → websocket}/index.js +12 -0
  16. package/dist/server/{server/websocket → websocket}/index.js.map +1 -1
  17. package/marktoflow-gui-2.0.0-alpha.5.tgz +0 -0
  18. package/package.json +20 -6
  19. package/scripts/flatten-dist.js +69 -0
  20. package/src/client/components/Canvas/Canvas.tsx +3 -1
  21. package/src/client/components/Canvas/ExecutionOverlay.tsx +120 -32
  22. package/src/client/components/Canvas/ForEachNode.tsx +27 -3
  23. package/src/client/components/Canvas/IfElseNode.tsx +22 -7
  24. package/src/client/components/Canvas/NodeContextMenu.tsx +8 -4
  25. package/src/client/components/Canvas/ParallelNode.tsx +25 -8
  26. package/src/client/components/Canvas/SwitchNode.tsx +41 -20
  27. package/src/client/components/Canvas/Toolbar.tsx +59 -21
  28. package/src/client/components/Canvas/TransformNode.tsx +9 -0
  29. package/src/client/components/Canvas/WhileNode.tsx +35 -3
  30. package/src/client/components/Debug/VariableInspector.tsx +148 -0
  31. package/src/client/components/Prompt/PromptInput.tsx +3 -1
  32. package/src/client/components/Settings/ProviderSwitcher.tsx +228 -0
  33. package/src/client/components/Sidebar/ImportDialog.tsx +257 -0
  34. package/src/client/components/Sidebar/Sidebar.tsx +21 -2
  35. package/src/client/components/common/KeyboardShortcuts.tsx +8 -2
  36. package/src/client/stores/agentStore.ts +109 -0
  37. package/src/client/stores/executionStore.ts +64 -2
  38. package/src/client/stores/workflowStore.ts +10 -2
  39. package/src/client/styles/globals.css +106 -0
  40. package/src/client/utils/platform.ts +46 -0
  41. package/src/client/utils/workflowToGraph.ts +245 -21
  42. package/src/server/index.ts +24 -1
  43. package/src/server/routes/executions.ts +136 -0
  44. package/src/server/routes/workflows.ts +42 -1
  45. package/src/server/services/WorkflowService.ts +176 -16
  46. package/src/server/websocket/index.ts +13 -0
  47. package/tests/unit/ForEachNode.test.tsx +96 -6
  48. package/tests/unit/IfElseNode.test.tsx +47 -0
  49. package/tests/unit/ParallelNode.test.tsx +80 -0
  50. package/tests/unit/SwitchNode.test.tsx +75 -0
  51. package/tests/unit/WhileNode.test.tsx +12 -8
  52. package/tests/unit/agentStore.test.ts +218 -0
  53. package/tests/unit/executionStore.test.ts +40 -0
  54. package/tests/unit/platform.test.ts +118 -0
  55. package/tests/unit/workflowToGraph.test.ts +22 -0
  56. package/dist/client/assets/index-C90Y_aBX.js +0 -678
  57. package/dist/client/assets/index-C90Y_aBX.js.map +0 -1
  58. package/dist/client/assets/index-CRWeQ3NN.css +0 -1
  59. package/dist/server/server/index.js.map +0 -1
  60. package/dist/server/server/routes/workflows.js.map +0 -1
  61. package/dist/server/server/services/WorkflowService.js.map +0 -1
  62. /package/dist/server/{server/routes → routes}/ai.js +0 -0
  63. /package/dist/server/{server/routes → routes}/ai.js.map +0 -0
  64. /package/dist/server/{server/routes → routes}/execute.js +0 -0
  65. /package/dist/server/{server/routes → routes}/execute.js.map +0 -0
  66. /package/dist/server/{server/routes → routes}/tools.js +0 -0
  67. /package/dist/server/{server/routes → routes}/tools.js.map +0 -0
  68. /package/dist/server/{server/services → services}/AIService.js +0 -0
  69. /package/dist/server/{server/services → services}/AIService.js.map +0 -0
  70. /package/dist/server/{server/services → services}/FileWatcher.js +0 -0
  71. /package/dist/server/{server/services → services}/FileWatcher.js.map +0 -0
  72. /package/dist/server/{server/services → services}/agents/claude-code-provider.js +0 -0
  73. /package/dist/server/{server/services → services}/agents/claude-code-provider.js.map +0 -0
  74. /package/dist/server/{server/services → services}/agents/claude-provider.js +0 -0
  75. /package/dist/server/{server/services → services}/agents/claude-provider.js.map +0 -0
  76. /package/dist/server/{server/services → services}/agents/codex-provider.js +0 -0
  77. /package/dist/server/{server/services → services}/agents/codex-provider.js.map +0 -0
  78. /package/dist/server/{server/services → services}/agents/copilot-provider.js +0 -0
  79. /package/dist/server/{server/services → services}/agents/copilot-provider.js.map +0 -0
  80. /package/dist/server/{server/services → services}/agents/demo-provider.js +0 -0
  81. /package/dist/server/{server/services → services}/agents/demo-provider.js.map +0 -0
  82. /package/dist/server/{server/services → services}/agents/index.js +0 -0
  83. /package/dist/server/{server/services → services}/agents/index.js.map +0 -0
  84. /package/dist/server/{server/services → services}/agents/ollama-provider.js +0 -0
  85. /package/dist/server/{server/services → services}/agents/ollama-provider.js.map +0 -0
  86. /package/dist/server/{server/services → services}/agents/prompts.js +0 -0
  87. /package/dist/server/{server/services → services}/agents/prompts.js.map +0 -0
  88. /package/dist/server/{server/services → services}/agents/registry.js +0 -0
  89. /package/dist/server/{server/services → services}/agents/registry.js.map +0 -0
  90. /package/dist/server/{server/services → services}/agents/types.js +0 -0
  91. /package/dist/server/{server/services → services}/agents/types.js.map +0 -0
  92. /package/dist/{server/shared → shared}/constants.js +0 -0
  93. /package/dist/{server/shared → shared}/constants.js.map +0 -0
  94. /package/dist/{server/shared → shared}/types.js +0 -0
  95. /package/dist/{server/shared → shared}/types.js.map +0 -0
@@ -8,6 +8,7 @@ export interface ExecutionStepResult {
8
8
  startTime?: string;
9
9
  endTime?: string;
10
10
  duration?: number;
11
+ inputs?: Record<string, unknown>;
11
12
  output?: unknown;
12
13
  outputVariable?: string;
13
14
  error?: string;
@@ -42,13 +43,14 @@ interface ExecutionState {
42
43
  currentRunId: string | null;
43
44
  isExecuting: boolean;
44
45
  isPaused: boolean;
46
+ isLoadingHistory: boolean;
45
47
 
46
48
  // Debug mode state
47
49
  debug: DebugState;
48
50
 
49
51
  // Existing methods
50
52
  startExecution: (workflowId: string, workflowName: string, inputs?: Record<string, unknown>) => string;
51
- updateStepStatus: (runId: string, stepId: string, status: StepStatus, output?: unknown, error?: string, outputVariable?: string) => void;
53
+ updateStepStatus: (runId: string, stepId: string, status: StepStatus, output?: unknown, error?: string, outputVariable?: string, inputs?: Record<string, unknown>) => void;
52
54
  completeExecution: (runId: string, status: WorkflowStatus, outputs?: Record<string, unknown>) => void;
53
55
  addLog: (runId: string, message: string) => void;
54
56
  pauseExecution: () => void;
@@ -57,6 +59,10 @@ interface ExecutionState {
57
59
  clearHistory: () => void;
58
60
  getRun: (runId: string) => ExecutionRun | undefined;
59
61
 
62
+ // API sync methods
63
+ loadHistory: (workflowId?: string) => Promise<void>;
64
+ syncRunWithBackend: (runId: string) => Promise<void>;
65
+
60
66
  // Debug mode methods
61
67
  enableDebugMode: () => void;
62
68
  disableDebugMode: () => void;
@@ -87,6 +93,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
87
93
  currentRunId: null,
88
94
  isExecuting: false,
89
95
  isPaused: false,
96
+ isLoadingHistory: false,
90
97
  debug: initialDebugState,
91
98
 
92
99
  startExecution: (workflowId, workflowName, inputs) => {
@@ -112,7 +119,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
112
119
  return runId;
113
120
  },
114
121
 
115
- updateStepStatus: (runId, stepId, status, output, error, outputVariable) => {
122
+ updateStepStatus: (runId, stepId, status, output, error, outputVariable, inputs) => {
116
123
  const { debug } = get();
117
124
 
118
125
  // Check if we need to pause at breakpoint
@@ -141,6 +148,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
141
148
  status,
142
149
  endTime,
143
150
  duration,
151
+ inputs: inputs ?? s.inputs,
144
152
  output: output ?? s.output,
145
153
  outputVariable: outputVariable ?? s.outputVariable,
146
154
  error: error ?? s.error,
@@ -157,6 +165,7 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
157
165
  stepName: stepId,
158
166
  status,
159
167
  startTime: now,
168
+ inputs,
160
169
  output,
161
170
  outputVariable,
162
171
  error,
@@ -411,6 +420,59 @@ export const useExecutionStore = create<ExecutionState>((set, get) => ({
411
420
  },
412
421
  });
413
422
  },
423
+
424
+ // API sync methods
425
+ loadHistory: async (workflowId) => {
426
+ set({ isLoadingHistory: true });
427
+ try {
428
+ const url = workflowId
429
+ ? `/api/executions?workflowId=${encodeURIComponent(workflowId)}&limit=50`
430
+ : '/api/executions?limit=50';
431
+
432
+ const response = await fetch(url);
433
+ if (!response.ok) {
434
+ throw new Error('Failed to load execution history');
435
+ }
436
+
437
+ const executions = await response.json();
438
+
439
+ // Convert backend ExecutionRecord format to ExecutionRun format
440
+ const runs: ExecutionRun[] = executions.map((exec: any) => ({
441
+ id: exec.runId,
442
+ workflowId: exec.workflowId,
443
+ workflowName: exec.workflowPath.split('/').pop()?.replace('.md', '') || exec.workflowId,
444
+ status: exec.status,
445
+ startTime: exec.startedAt,
446
+ endTime: exec.completedAt || undefined,
447
+ duration: exec.completedAt
448
+ ? new Date(exec.completedAt).getTime() - new Date(exec.startedAt).getTime()
449
+ : undefined,
450
+ steps: [],
451
+ logs: [],
452
+ inputs: exec.inputs || undefined,
453
+ outputs: exec.outputs || undefined,
454
+ }));
455
+
456
+ // Merge with existing runs, avoiding duplicates
457
+ const existingRunIds = new Set(get().runs.map((r) => r.id));
458
+ const newRuns = runs.filter((r) => !existingRunIds.has(r.id));
459
+
460
+ set({
461
+ runs: [...get().runs, ...newRuns].slice(0, 50),
462
+ isLoadingHistory: false,
463
+ });
464
+ } catch (error) {
465
+ console.error('Error loading execution history:', error);
466
+ set({ isLoadingHistory: false });
467
+ }
468
+ },
469
+
470
+ syncRunWithBackend: async (runId) => {
471
+ // This would be called to persist a run to the backend
472
+ // For now, the backend integration will happen via WebSocket events
473
+ // This is a placeholder for future direct API sync
474
+ console.log('Syncing run with backend:', runId);
475
+ },
414
476
  }));
415
477
 
416
478
  // Helper to format duration
@@ -9,6 +9,7 @@ interface WorkflowState {
9
9
  error: string | null;
10
10
 
11
11
  loadWorkflows: () => Promise<void>;
12
+ fetchWorkflows: () => Promise<void>; // Alias for compatibility
12
13
  selectWorkflow: (path: string) => void;
13
14
  loadWorkflow: (path: string) => Promise<void>;
14
15
  saveWorkflow: (workflow: Workflow) => Promise<void>;
@@ -85,8 +86,8 @@ const demoWorkflow: Workflow = {
85
86
 
86
87
  export const useWorkflowStore = create<WorkflowState>((set, get) => ({
87
88
  workflows: demoWorkflows,
88
- selectedWorkflow: demoWorkflows[0].path,
89
- currentWorkflow: demoWorkflow,
89
+ selectedWorkflow: null,
90
+ currentWorkflow: null,
90
91
  isLoading: false,
91
92
  error: null,
92
93
 
@@ -96,13 +97,20 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
96
97
  const response = await fetch('/api/workflows');
97
98
  if (!response.ok) throw new Error('Failed to load workflows');
98
99
  const data = await response.json();
100
+ console.log('Loaded workflows:', data.workflows.length);
99
101
  set({ workflows: data.workflows, isLoading: false });
100
102
  } catch (error) {
103
+ console.error('Failed to load workflows, using demo data:', error);
101
104
  // Use demo data if API fails
102
105
  set({ workflows: demoWorkflows, isLoading: false });
103
106
  }
104
107
  },
105
108
 
109
+ // Alias for compatibility with components
110
+ fetchWorkflows: async () => {
111
+ await get().loadWorkflows();
112
+ },
113
+
106
114
  selectWorkflow: (path) => {
107
115
  set({ selectedWorkflow: path });
108
116
  get().loadWorkflow(path);
@@ -82,6 +82,112 @@
82
82
  border-color: #d9534f;
83
83
  }
84
84
 
85
+ /* Control flow node base styles */
86
+ .control-flow-node {
87
+ min-width: 220px;
88
+ border-radius: 12px;
89
+ border: 2px solid transparent;
90
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
91
+ transition: all 0.2s ease;
92
+ overflow: hidden;
93
+ }
94
+
95
+ .control-flow-node:hover {
96
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
97
+ transform: translateY(-1px);
98
+ }
99
+
100
+ .control-flow-node.selected {
101
+ box-shadow: 0 0 0 4px rgba(255, 109, 90, 0.3), 0 4px 16px rgba(0, 0, 0, 0.4);
102
+ border-color: rgba(255, 109, 90, 0.5);
103
+ }
104
+
105
+ .control-flow-node.running {
106
+ animation: pulse 1.5s ease-in-out infinite;
107
+ }
108
+
109
+ .control-flow-node.completed {
110
+ box-shadow: 0 0 0 2px rgba(92, 184, 92, 0.4), 0 4px 16px rgba(0, 0, 0, 0.4);
111
+ }
112
+
113
+ .control-flow-node.failed {
114
+ box-shadow: 0 0 0 2px rgba(217, 83, 79, 0.4), 0 4px 16px rgba(0, 0, 0, 0.4);
115
+ }
116
+
117
+ /* Control flow specific node types */
118
+ .if-else-node {
119
+ position: relative;
120
+ }
121
+
122
+ .switch-node {
123
+ position: relative;
124
+ }
125
+
126
+ .for-each-node,
127
+ .while-node {
128
+ position: relative;
129
+ }
130
+
131
+ .parallel-node {
132
+ position: relative;
133
+ }
134
+
135
+ .try-catch-node {
136
+ position: relative;
137
+ }
138
+
139
+ .transform-node {
140
+ position: relative;
141
+ }
142
+
143
+ /* Execution overlays for control flow nodes */
144
+ .control-flow-overlay {
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ right: 0;
149
+ bottom: 0;
150
+ background: rgba(0, 0, 0, 0.7);
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ z-index: 10;
155
+ pointer-events: none;
156
+ animation: fadeIn 0.2s ease-out;
157
+ }
158
+
159
+ .control-flow-badge {
160
+ display: inline-flex;
161
+ align-items: center;
162
+ gap: 4px;
163
+ padding: 4px 8px;
164
+ border-radius: 6px;
165
+ font-size: 10px;
166
+ font-weight: 600;
167
+ text-transform: uppercase;
168
+ letter-spacing: 0.5px;
169
+ background: rgba(0, 0, 0, 0.5);
170
+ backdrop-filter: blur(4px);
171
+ }
172
+
173
+ .control-flow-badge.early-exit {
174
+ background: rgba(255, 152, 0, 0.2);
175
+ border: 1px solid rgba(255, 152, 0, 0.5);
176
+ color: #ff9800;
177
+ }
178
+
179
+ .control-flow-badge.skipped {
180
+ background: rgba(128, 128, 128, 0.2);
181
+ border: 1px solid rgba(128, 128, 128, 0.5);
182
+ color: #999;
183
+ }
184
+
185
+ .control-flow-badge.max-concurrent {
186
+ background: rgba(255, 193, 7, 0.2);
187
+ border: 1px solid rgba(255, 193, 7, 0.5);
188
+ color: #ffc107;
189
+ }
190
+
85
191
  /* Edge animation */
86
192
  .react-flow__edge-path.animated {
87
193
  stroke-dasharray: 8;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Platform detection utilities for cross-platform keyboard shortcuts
3
+ */
4
+
5
+ /**
6
+ * Detect if the user is on a Mac platform
7
+ */
8
+ export const isMac = (): boolean => {
9
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
10
+ return false;
11
+ }
12
+ return navigator.platform.toUpperCase().includes('MAC');
13
+ };
14
+
15
+ /**
16
+ * Get the appropriate modifier key display for the current platform
17
+ * Returns '⌘' on Mac, 'Ctrl' on Windows/Linux
18
+ */
19
+ export const getModKey = (): string => {
20
+ return isMac() ? '⌘' : 'Ctrl';
21
+ };
22
+
23
+ /**
24
+ * Convert a shortcut string to display the correct modifier for the platform
25
+ * @param shortcut - The shortcut string (can contain ⌘, Cmd, or Ctrl)
26
+ * @returns The shortcut with the correct platform modifier
27
+ *
28
+ * @example
29
+ * getShortcutDisplay('⌘+S') // '⌘+S' on Mac, 'Ctrl+S' on Windows
30
+ * getShortcutDisplay('Cmd+Z') // '⌘+Z' on Mac, 'Ctrl+Z' on Windows
31
+ */
32
+ export const getShortcutDisplay = (shortcut: string): string => {
33
+ const modKey = getModKey();
34
+
35
+ return shortcut
36
+ .replace(/⌘/g, modKey)
37
+ .replace(/Cmd/g, modKey);
38
+ };
39
+
40
+ /**
41
+ * Check if the modifier key is pressed in a keyboard event
42
+ * Checks metaKey on Mac, ctrlKey on Windows/Linux
43
+ */
44
+ export const isModKeyPressed = (event: KeyboardEvent | React.KeyboardEvent): boolean => {
45
+ return isMac() ? event.metaKey : event.ctrlKey;
46
+ };
@@ -5,9 +5,15 @@ interface WorkflowStep {
5
5
  name?: string;
6
6
  action?: string;
7
7
  workflow?: string;
8
+ type?: 'while' | 'for_each' | 'for' | 'switch' | 'parallel' | 'try' | 'if' | 'map' | 'filter' | 'reduce';
9
+ condition?: string;
10
+ items?: string;
11
+ maxIterations?: number;
8
12
  inputs: Record<string, unknown>;
9
13
  outputVariable?: string;
10
14
  conditions?: string[];
15
+ steps?: WorkflowStep[];
16
+ variables?: Record<string, { initial: unknown }>;
11
17
  }
12
18
 
13
19
  interface WorkflowTrigger {
@@ -31,17 +37,119 @@ interface GraphResult {
31
37
  edges: Edge[];
32
38
  }
33
39
 
40
+ /**
41
+ * Parse control flow constructs from raw markdown content
42
+ * This is a temporary solution until the core parser supports control flow
43
+ */
44
+ function extractControlFlowFromMarkdown(markdown?: string): WorkflowStep[] {
45
+ if (!markdown) return [];
46
+
47
+ const controlFlowSteps: WorkflowStep[] = [];
48
+ // Match YAML code blocks that contain control flow types
49
+ const codeBlockRegex = /```yaml\s*\n([\s\S]*?)\n```/g;
50
+ let match;
51
+ let stepIndex = 0;
52
+
53
+ while ((match = codeBlockRegex.exec(markdown)) !== null) {
54
+ const yamlContent = match[1];
55
+
56
+ // Check if this is a control flow block
57
+ const typeMatch = yamlContent.match(/^type:\s*(while|for_each|for|switch|parallel|try|if|map|filter|reduce)/m);
58
+ if (typeMatch) {
59
+ const type = typeMatch[1] as WorkflowStep['type'];
60
+ const id = `control-flow-${type}-${stepIndex++}`;
61
+
62
+ // Extract common properties
63
+ const conditionMatch = yamlContent.match(/condition:\s*["'](.+?)["']/);
64
+ const itemsMatch = yamlContent.match(/items:\s*["'](.+?)["']/);
65
+ const maxIterMatch = yamlContent.match(/max_iterations:\s*(\d+)/);
66
+ const expressionMatch = yamlContent.match(/expression:\s*["'](.+?)["']/);
67
+ const itemVarMatch = yamlContent.match(/item_variable:\s*(\w+)/);
68
+
69
+ // Extract switch expression
70
+ const switchExprMatch = yamlContent.match(/expression:\s*["'](.+?)["']/);
71
+
72
+ // Extract variables for while loops
73
+ const variables: Record<string, { initial: unknown }> = {};
74
+ const varsMatch = yamlContent.match(/variables:\s*\n((?: .*\n)*)/);
75
+ if (varsMatch) {
76
+ const varsContent = varsMatch[1];
77
+ const varLines = varsContent.split('\n').filter(Boolean);
78
+ varLines.forEach(line => {
79
+ const varMatch = line.match(/^\s+(\w+):\s*$/);
80
+ if (varMatch) {
81
+ const varName = varMatch[1];
82
+ // Try to find initial value on next line
83
+ const initMatch = varsContent.match(new RegExp(`${varName}:\\s*\\n\\s+initial:\\s*(.+)`));
84
+ if (initMatch) {
85
+ try {
86
+ variables[varName] = { initial: JSON.parse(initMatch[1]) };
87
+ } catch {
88
+ variables[varName] = { initial: initMatch[1].trim() };
89
+ }
90
+ }
91
+ }
92
+ });
93
+ }
94
+
95
+ // Build step data based on type
96
+ const step: WorkflowStep = {
97
+ id,
98
+ type,
99
+ name: type === 'map' ? 'Map Transform' :
100
+ type === 'filter' ? 'Filter Transform' :
101
+ type === 'reduce' ? 'Reduce Transform' :
102
+ type === 'switch' ? 'Switch/Case' :
103
+ type === 'parallel' ? 'Parallel Execution' :
104
+ type === 'try' ? 'Try/Catch' :
105
+ type === 'if' ? 'If/Else' :
106
+ `${type.charAt(0).toUpperCase() + type.slice(1)} Loop`,
107
+ inputs: {},
108
+ };
109
+
110
+ // Add type-specific properties
111
+ if (conditionMatch) step.condition = conditionMatch[1];
112
+ if (itemsMatch) step.items = itemsMatch[1];
113
+ if (maxIterMatch) step.maxIterations = parseInt(maxIterMatch[1], 10);
114
+ if (Object.keys(variables).length > 0) step.variables = variables;
115
+
116
+ // Transform-specific properties
117
+ if (type === 'map' || type === 'filter' || type === 'reduce') {
118
+ if (itemVarMatch) {
119
+ step.inputs = { ...step.inputs, itemVariable: itemVarMatch[1] };
120
+ }
121
+ if (expressionMatch) {
122
+ step.inputs = { ...step.inputs, expression: expressionMatch[1] };
123
+ }
124
+ }
125
+
126
+ // Switch-specific properties
127
+ if (type === 'switch' && switchExprMatch) {
128
+ step.inputs = { ...step.inputs, expression: switchExprMatch[1] };
129
+ }
130
+
131
+ controlFlowSteps.push(step);
132
+ }
133
+ }
134
+
135
+ return controlFlowSteps;
136
+ }
137
+
34
138
  /**
35
139
  * Converts a marktoflow Workflow to React Flow nodes and edges
36
140
  */
37
- export function workflowToGraph(workflow: Workflow): GraphResult {
141
+ export function workflowToGraph(workflow: Workflow & { markdown?: string }): GraphResult {
38
142
  const nodes: Node[] = [];
39
143
  const edges: Edge[] = [];
40
144
 
41
- const VERTICAL_SPACING = 120;
145
+ const VERTICAL_SPACING = 180;
42
146
  const HORIZONTAL_OFFSET = 250;
43
147
  let currentY = 0;
44
148
 
149
+ // Try to extract control flow from markdown if available
150
+ const controlFlowSteps = extractControlFlowFromMarkdown(workflow.markdown);
151
+ const allSteps = [...workflow.steps, ...controlFlowSteps];
152
+
45
153
  // Add trigger node if triggers are defined
46
154
  if (workflow.triggers && workflow.triggers.length > 0) {
47
155
  const trigger = workflow.triggers[0]; // Primary trigger
@@ -65,11 +173,11 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
65
173
  currentY += VERTICAL_SPACING;
66
174
 
67
175
  // Edge from trigger to first step
68
- if (workflow.steps.length > 0) {
176
+ if (allSteps.length > 0) {
69
177
  edges.push({
70
- id: `e-${triggerId}-${workflow.steps[0].id}`,
178
+ id: `e-${triggerId}-${allSteps[0].id}`,
71
179
  source: triggerId,
72
- target: workflow.steps[0].id,
180
+ target: allSteps[0].id,
73
181
  type: 'smoothstep',
74
182
  animated: false,
75
183
  style: { stroke: '#ff6d5a', strokeWidth: 2 },
@@ -78,30 +186,101 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
78
186
  }
79
187
 
80
188
  // Create nodes for each step
81
- workflow.steps.forEach((step, index) => {
189
+ allSteps.forEach((step, index) => {
82
190
  const isSubWorkflow = !!step.workflow;
191
+ const isControlFlow = !!step.type && ['while', 'for_each', 'for', 'switch', 'parallel', 'try', 'if', 'map', 'filter', 'reduce'].includes(step.type);
192
+
193
+ let nodeType = 'step';
194
+ if (isSubWorkflow) {
195
+ nodeType = 'subworkflow';
196
+ } else if (isControlFlow) {
197
+ nodeType = step.type!;
198
+ }
199
+
200
+ // Build node data based on type
201
+ const baseData = {
202
+ id: step.id,
203
+ name: step.name,
204
+ action: step.action,
205
+ workflowPath: step.workflow,
206
+ status: 'pending' as const,
207
+ };
208
+
209
+ // Add control-flow specific data
210
+ let nodeData = { ...baseData };
211
+ if (step.type === 'while') {
212
+ nodeData = {
213
+ ...baseData,
214
+ condition: step.condition || 'true',
215
+ maxIterations: step.maxIterations || 100,
216
+ variables: step.variables,
217
+ };
218
+ } else if (step.type === 'for_each' || step.type === 'for') {
219
+ nodeData = {
220
+ ...baseData,
221
+ items: step.items || '[]',
222
+ itemVariable: step.inputs?.itemVariable as string,
223
+ };
224
+ } else if (step.type === 'switch') {
225
+ nodeData = {
226
+ ...baseData,
227
+ expression: step.inputs?.expression as string || step.condition || '',
228
+ cases: {},
229
+ hasDefault: true,
230
+ };
231
+ } else if (step.type === 'parallel') {
232
+ nodeData = {
233
+ ...baseData,
234
+ branches: [],
235
+ maxConcurrent: 0,
236
+ };
237
+ } else if (step.type === 'try') {
238
+ nodeData = {
239
+ ...baseData,
240
+ hasCatch: true,
241
+ hasFinally: false,
242
+ };
243
+ } else if (step.type === 'if') {
244
+ nodeData = {
245
+ ...baseData,
246
+ condition: step.condition || 'true',
247
+ hasElse: true,
248
+ };
249
+ } else if (step.type === 'map' || step.type === 'filter' || step.type === 'reduce') {
250
+ nodeData = {
251
+ ...baseData,
252
+ transformType: step.type,
253
+ items: step.items || '[]',
254
+ itemVariable: step.inputs?.itemVariable as string,
255
+ expression: step.inputs?.expression as string,
256
+ condition: step.condition,
257
+ };
258
+ } else {
259
+ // Regular step
260
+ nodeData = {
261
+ ...baseData,
262
+ condition: step.condition,
263
+ items: step.items,
264
+ maxIterations: step.maxIterations,
265
+ variables: step.variables,
266
+ };
267
+ }
83
268
 
84
269
  const node: Node = {
85
270
  id: step.id,
86
- type: isSubWorkflow ? 'subworkflow' : 'step',
271
+ type: nodeType,
87
272
  position: {
88
273
  x: HORIZONTAL_OFFSET,
89
274
  y: currentY + index * VERTICAL_SPACING,
90
275
  },
91
- data: {
92
- id: step.id,
93
- name: step.name,
94
- action: step.action,
95
- workflowPath: step.workflow,
96
- status: 'pending',
97
- },
276
+ data: nodeData,
98
277
  };
99
278
 
100
279
  nodes.push(node);
101
280
 
102
281
  // Create edge to next step
103
- if (index < workflow.steps.length - 1) {
104
- const nextStep = workflow.steps[index + 1];
282
+ if (index < allSteps.length - 1) {
283
+ const nextStep = allSteps[index + 1];
105
284
  const edge: Edge = {
106
285
  id: `e-${step.id}-${nextStep.id}`,
107
286
  source: step.id,
@@ -120,16 +299,61 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
120
299
 
121
300
  edges.push(edge);
122
301
  }
302
+
303
+ // Add loop-back edge for loops
304
+ if (step.type === 'while' || step.type === 'for_each' || step.type === 'for') {
305
+ const loopColor = step.type === 'while' ? '#fb923c' : '#f093fb';
306
+ edges.push({
307
+ id: `e-${step.id}-loop-back`,
308
+ source: step.id,
309
+ target: step.id,
310
+ sourceHandle: 'loop-back',
311
+ type: 'smoothstep',
312
+ animated: true,
313
+ style: {
314
+ stroke: loopColor,
315
+ strokeWidth: 2,
316
+ strokeDasharray: '5,5',
317
+ },
318
+ label: step.type === 'while' ? 'while true' : 'for each item',
319
+ labelStyle: { fill: loopColor, fontSize: 9 },
320
+ labelBgStyle: { fill: '#1a1a2e', fillOpacity: 0.9 },
321
+ });
322
+ }
323
+
324
+ // Add iteration indicator for transform operations (map/filter/reduce)
325
+ if (step.type === 'map' || step.type === 'filter' || step.type === 'reduce') {
326
+ const transformColor = '#14b8a6';
327
+ const label = step.type === 'map' ? 'transform each' :
328
+ step.type === 'filter' ? 'test each' :
329
+ 'accumulate';
330
+ edges.push({
331
+ id: `e-${step.id}-transform-flow`,
332
+ source: step.id,
333
+ target: step.id,
334
+ sourceHandle: 'loop-back',
335
+ type: 'smoothstep',
336
+ animated: true,
337
+ style: {
338
+ stroke: transformColor,
339
+ strokeWidth: 1.5,
340
+ strokeDasharray: '3,3',
341
+ },
342
+ label,
343
+ labelStyle: { fill: transformColor, fontSize: 8 },
344
+ labelBgStyle: { fill: '#1a1a2e', fillOpacity: 0.9 },
345
+ });
346
+ }
123
347
  });
124
348
 
125
349
  // Add output node at the end
126
- if (workflow.steps.length > 0) {
350
+ if (allSteps.length > 0) {
127
351
  const outputId = `output-${workflow.metadata.id}`;
128
- const lastStep = workflow.steps[workflow.steps.length - 1];
129
- const outputY = currentY + workflow.steps.length * VERTICAL_SPACING;
352
+ const lastStep = allSteps[allSteps.length - 1];
353
+ const outputY = currentY + allSteps.length * VERTICAL_SPACING;
130
354
 
131
355
  // Collect all output variables
132
- const outputVariables = workflow.steps
356
+ const outputVariables = allSteps
133
357
  .filter((s) => s.outputVariable)
134
358
  .map((s) => s.outputVariable as string);
135
359
 
@@ -157,7 +381,7 @@ export function workflowToGraph(workflow: Workflow): GraphResult {
157
381
  }
158
382
 
159
383
  // Add data flow edges based on variable references
160
- const variableEdges = findVariableDependencies(workflow.steps);
384
+ const variableEdges = findVariableDependencies(allSteps);
161
385
  edges.push(...variableEdges);
162
386
 
163
387
  return { nodes, edges };