@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,847 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Play,
4
+ Pause,
5
+ SkipForward,
6
+ Square,
7
+ CheckCircle,
8
+ XCircle,
9
+ Loader2,
10
+ ChevronDown,
11
+ ChevronRight,
12
+ Copy,
13
+ Check,
14
+ Bug,
15
+ Circle,
16
+ ArrowRight,
17
+ ArrowDown,
18
+ ArrowUp,
19
+ Trash2,
20
+ Plus,
21
+ X,
22
+ } from 'lucide-react';
23
+ import { Button } from '../common/Button';
24
+ import type { StepStatus, WorkflowStatus } from '@shared/types';
25
+ import type { DebugState } from '../../stores/executionStore';
26
+
27
+ interface ExecutionStep {
28
+ stepId: string;
29
+ stepName: string;
30
+ status: StepStatus;
31
+ duration?: number;
32
+ error?: string;
33
+ output?: unknown;
34
+ outputVariable?: string;
35
+ }
36
+
37
+ interface ExecutionOverlayProps {
38
+ isExecuting: boolean;
39
+ isPaused: boolean;
40
+ workflowStatus: WorkflowStatus;
41
+ currentStepId: string | null;
42
+ steps: ExecutionStep[];
43
+ logs: string[];
44
+ onPause: () => void;
45
+ onResume: () => void;
46
+ onStop: () => void;
47
+ onStepOver: () => void;
48
+ onClose: () => void;
49
+ // Debug props
50
+ debug?: DebugState;
51
+ onToggleDebugMode?: () => void;
52
+ onToggleBreakpoint?: (stepId: string) => void;
53
+ onStepInto?: () => void;
54
+ onStepOut?: () => void;
55
+ onClearBreakpoints?: () => void;
56
+ onAddWatchExpression?: (expression: string) => void;
57
+ onRemoveWatchExpression?: (expression: string) => void;
58
+ }
59
+
60
+ export function ExecutionOverlay({
61
+ isExecuting,
62
+ isPaused,
63
+ workflowStatus,
64
+ currentStepId,
65
+ steps,
66
+ logs,
67
+ onPause,
68
+ onResume,
69
+ onStop,
70
+ onStepOver,
71
+ onClose,
72
+ // Debug props
73
+ debug,
74
+ onToggleDebugMode,
75
+ onToggleBreakpoint,
76
+ onStepInto,
77
+ onStepOut,
78
+ onClearBreakpoints,
79
+ onAddWatchExpression,
80
+ onRemoveWatchExpression,
81
+ }: ExecutionOverlayProps) {
82
+ const [activeTab, setActiveTab] = useState<'steps' | 'variables' | 'logs' | 'debug'>('steps');
83
+ const isDebugEnabled = debug?.enabled ?? false;
84
+
85
+ const completedSteps = steps.filter((s) => s.status === 'completed').length;
86
+ const failedSteps = steps.filter((s) => s.status === 'failed').length;
87
+ const progress = steps.length > 0 ? (completedSteps / steps.length) * 100 : 0;
88
+
89
+ if (!isExecuting && workflowStatus === 'pending') {
90
+ return null;
91
+ }
92
+
93
+ return (
94
+ <div className="absolute bottom-20 left-4 right-4 z-20 bg-panel-bg border border-node-border rounded-lg shadow-xl max-h-[400px] flex flex-col">
95
+ {/* Header */}
96
+ <div className="flex items-center justify-between px-4 py-3 border-b border-node-border">
97
+ <div className="flex items-center gap-3">
98
+ <StatusIcon status={workflowStatus} />
99
+ <div>
100
+ <div className="text-sm font-medium text-white">
101
+ {getStatusText(workflowStatus)}
102
+ </div>
103
+ <div className="text-xs text-gray-400">
104
+ {completedSteps}/{steps.length} steps completed
105
+ {failedSteps > 0 && ` • ${failedSteps} failed`}
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Controls */}
111
+ <div className="flex items-center gap-2">
112
+ {/* Debug mode toggle */}
113
+ {onToggleDebugMode && (
114
+ <Button
115
+ variant={isDebugEnabled ? 'primary' : 'ghost'}
116
+ size="sm"
117
+ onClick={onToggleDebugMode}
118
+ icon={<Bug className="w-4 h-4" />}
119
+ title={isDebugEnabled ? 'Disable debug mode' : 'Enable debug mode'}
120
+ >
121
+ Debug
122
+ </Button>
123
+ )}
124
+
125
+ {isExecuting && (
126
+ <>
127
+ {isPaused ? (
128
+ <Button
129
+ variant="secondary"
130
+ size="sm"
131
+ onClick={onResume}
132
+ icon={<Play className="w-4 h-4" />}
133
+ >
134
+ Resume
135
+ </Button>
136
+ ) : (
137
+ <Button
138
+ variant="secondary"
139
+ size="sm"
140
+ onClick={onPause}
141
+ icon={<Pause className="w-4 h-4" />}
142
+ >
143
+ Pause
144
+ </Button>
145
+ )}
146
+
147
+ {/* Debug stepping controls */}
148
+ {isDebugEnabled && isPaused && (
149
+ <>
150
+ <Button
151
+ variant="secondary"
152
+ size="sm"
153
+ onClick={onStepOver}
154
+ icon={<ArrowRight className="w-4 h-4" />}
155
+ title="Step Over (F10)"
156
+ >
157
+ Over
158
+ </Button>
159
+ {onStepInto && (
160
+ <Button
161
+ variant="secondary"
162
+ size="sm"
163
+ onClick={onStepInto}
164
+ icon={<ArrowDown className="w-4 h-4" />}
165
+ title="Step Into (F11)"
166
+ >
167
+ Into
168
+ </Button>
169
+ )}
170
+ {onStepOut && (
171
+ <Button
172
+ variant="secondary"
173
+ size="sm"
174
+ onClick={onStepOut}
175
+ icon={<ArrowUp className="w-4 h-4" />}
176
+ title="Step Out (Shift+F11)"
177
+ >
178
+ Out
179
+ </Button>
180
+ )}
181
+ </>
182
+ )}
183
+
184
+ {/* Regular step control when not in debug mode */}
185
+ {!isDebugEnabled && (
186
+ <Button
187
+ variant="secondary"
188
+ size="sm"
189
+ onClick={onStepOver}
190
+ icon={<SkipForward className="w-4 h-4" />}
191
+ disabled={!isPaused}
192
+ >
193
+ Step
194
+ </Button>
195
+ )}
196
+
197
+ <Button
198
+ variant="destructive"
199
+ size="sm"
200
+ onClick={onStop}
201
+ icon={<Square className="w-4 h-4" />}
202
+ >
203
+ Stop
204
+ </Button>
205
+ </>
206
+ )}
207
+ {!isExecuting && (
208
+ <Button variant="secondary" size="sm" onClick={onClose}>
209
+ Close
210
+ </Button>
211
+ )}
212
+ </div>
213
+ </div>
214
+
215
+ {/* Progress bar */}
216
+ <div className="h-1 bg-node-bg">
217
+ <div
218
+ className={`h-full transition-all duration-300 ${
219
+ workflowStatus === 'failed'
220
+ ? 'bg-error'
221
+ : workflowStatus === 'completed'
222
+ ? 'bg-success'
223
+ : 'bg-primary'
224
+ }`}
225
+ style={{ width: `${progress}%` }}
226
+ />
227
+ </div>
228
+
229
+ {/* Tabs */}
230
+ <div className="flex border-b border-node-border">
231
+ <button
232
+ onClick={() => setActiveTab('steps')}
233
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
234
+ activeTab === 'steps'
235
+ ? 'text-primary border-b-2 border-primary -mb-px'
236
+ : 'text-gray-400 hover:text-white'
237
+ }`}
238
+ >
239
+ Steps
240
+ </button>
241
+ <button
242
+ onClick={() => setActiveTab('variables')}
243
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
244
+ activeTab === 'variables'
245
+ ? 'text-primary border-b-2 border-primary -mb-px'
246
+ : 'text-gray-400 hover:text-white'
247
+ }`}
248
+ >
249
+ Variables
250
+ </button>
251
+ <button
252
+ onClick={() => setActiveTab('logs')}
253
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
254
+ activeTab === 'logs'
255
+ ? 'text-primary border-b-2 border-primary -mb-px'
256
+ : 'text-gray-400 hover:text-white'
257
+ }`}
258
+ >
259
+ Logs
260
+ </button>
261
+ {isDebugEnabled && (
262
+ <button
263
+ onClick={() => setActiveTab('debug')}
264
+ className={`px-4 py-2 text-sm font-medium transition-colors flex items-center gap-1 ${
265
+ activeTab === 'debug'
266
+ ? 'text-primary border-b-2 border-primary -mb-px'
267
+ : 'text-gray-400 hover:text-white'
268
+ }`}
269
+ >
270
+ <Bug className="w-3 h-3" />
271
+ Debug
272
+ </button>
273
+ )}
274
+ </div>
275
+
276
+ {/* Content */}
277
+ <div className="flex-1 overflow-y-auto p-4">
278
+ {activeTab === 'steps' && (
279
+ <StepsList
280
+ steps={steps}
281
+ currentStepId={currentStepId}
282
+ debugEnabled={isDebugEnabled}
283
+ breakpoints={debug?.breakpoints}
284
+ onToggleBreakpoint={onToggleBreakpoint}
285
+ />
286
+ )}
287
+ {activeTab === 'variables' && (
288
+ <VariableInspector steps={steps} />
289
+ )}
290
+ {activeTab === 'logs' && (
291
+ <LogsViewer logs={logs} />
292
+ )}
293
+ {activeTab === 'debug' && isDebugEnabled && (
294
+ <DebugPanel
295
+ debug={debug!}
296
+ onClearBreakpoints={onClearBreakpoints}
297
+ onAddWatchExpression={onAddWatchExpression}
298
+ onRemoveWatchExpression={onRemoveWatchExpression}
299
+ />
300
+ )}
301
+ </div>
302
+ </div>
303
+ );
304
+ }
305
+
306
+ function StepsList({
307
+ steps,
308
+ currentStepId,
309
+ debugEnabled,
310
+ breakpoints,
311
+ onToggleBreakpoint,
312
+ }: {
313
+ steps: ExecutionStep[];
314
+ currentStepId: string | null;
315
+ debugEnabled?: boolean;
316
+ breakpoints?: Set<string>;
317
+ onToggleBreakpoint?: (stepId: string) => void;
318
+ }) {
319
+ return (
320
+ <div className="space-y-2">
321
+ {steps.map((step) => {
322
+ const hasBreakpoint = breakpoints?.has(step.stepId);
323
+
324
+ return (
325
+ <div
326
+ key={step.stepId}
327
+ className={`flex items-center gap-3 p-3 rounded-lg border ${
328
+ step.stepId === currentStepId
329
+ ? 'bg-primary/10 border-primary'
330
+ : hasBreakpoint
331
+ ? 'bg-error/10 border-error/50'
332
+ : 'bg-node-bg border-node-border'
333
+ }`}
334
+ >
335
+ {/* Breakpoint indicator/toggle */}
336
+ {debugEnabled && onToggleBreakpoint && (
337
+ <button
338
+ onClick={() => onToggleBreakpoint(step.stepId)}
339
+ className={`w-4 h-4 rounded-full flex items-center justify-center transition-colors ${
340
+ hasBreakpoint
341
+ ? 'bg-error'
342
+ : 'bg-transparent border border-gray-500 hover:border-error hover:bg-error/20'
343
+ }`}
344
+ title={hasBreakpoint ? 'Remove breakpoint' : 'Add breakpoint'}
345
+ >
346
+ {hasBreakpoint && <Circle className="w-2 h-2 fill-current text-white" />}
347
+ </button>
348
+ )}
349
+
350
+ <StepStatusIcon status={step.status} />
351
+ <div className="flex-1 min-w-0">
352
+ <div className="text-sm font-medium text-white truncate">
353
+ {step.stepName || step.stepId}
354
+ </div>
355
+ {step.error && (
356
+ <div className="text-xs text-error mt-1 truncate">{step.error}</div>
357
+ )}
358
+ </div>
359
+ {step.duration !== undefined && (
360
+ <div className="text-xs text-gray-400">{step.duration}ms</div>
361
+ )}
362
+ </div>
363
+ );
364
+ })}
365
+ </div>
366
+ );
367
+ }
368
+
369
+ function LogsViewer({ logs }: { logs: string[] }) {
370
+ return (
371
+ <div className="font-mono text-xs space-y-1">
372
+ {logs.length === 0 ? (
373
+ <div className="text-gray-500">No logs yet...</div>
374
+ ) : (
375
+ logs.map((log, index) => (
376
+ <div key={index} className="text-gray-300">
377
+ {log}
378
+ </div>
379
+ ))
380
+ )}
381
+ </div>
382
+ );
383
+ }
384
+
385
+ function VariableInspector({ steps }: { steps: ExecutionStep[] }) {
386
+ const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
387
+ const [copiedKey, setCopiedKey] = useState<string | null>(null);
388
+
389
+ // Filter steps that have output data
390
+ const stepsWithOutput = steps.filter(
391
+ (step) => step.output !== undefined && step.outputVariable
392
+ );
393
+
394
+ const toggleStep = (stepId: string) => {
395
+ setExpandedSteps((prev) => {
396
+ const next = new Set(prev);
397
+ if (next.has(stepId)) {
398
+ next.delete(stepId);
399
+ } else {
400
+ next.add(stepId);
401
+ }
402
+ return next;
403
+ });
404
+ };
405
+
406
+ const copyValue = async (key: string, value: unknown) => {
407
+ try {
408
+ const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
409
+ await navigator.clipboard.writeText(text);
410
+ setCopiedKey(key);
411
+ setTimeout(() => setCopiedKey(null), 2000);
412
+ } catch (error) {
413
+ console.error('Failed to copy:', error);
414
+ }
415
+ };
416
+
417
+ if (stepsWithOutput.length === 0) {
418
+ return (
419
+ <div className="text-center py-8 text-gray-500 text-sm">
420
+ No variables available yet.
421
+ <br />
422
+ <span className="text-xs">Variables will appear as steps complete.</span>
423
+ </div>
424
+ );
425
+ }
426
+
427
+ return (
428
+ <div className="space-y-2">
429
+ {stepsWithOutput.map((step) => {
430
+ const isExpanded = expandedSteps.has(step.stepId);
431
+ return (
432
+ <div
433
+ key={step.stepId}
434
+ className="border border-node-border rounded-lg overflow-hidden"
435
+ >
436
+ {/* Variable Header */}
437
+ <button
438
+ onClick={() => toggleStep(step.stepId)}
439
+ className="w-full flex items-center gap-2 px-3 py-2 bg-node-bg hover:bg-white/5 transition-colors"
440
+ >
441
+ {isExpanded ? (
442
+ <ChevronDown className="w-4 h-4 text-gray-400" />
443
+ ) : (
444
+ <ChevronRight className="w-4 h-4 text-gray-400" />
445
+ )}
446
+ <code className="text-sm text-primary font-mono">
447
+ {step.outputVariable}
448
+ </code>
449
+ <span className="text-xs text-gray-500 ml-auto">
450
+ {getTypeLabel(step.output)}
451
+ </span>
452
+ </button>
453
+
454
+ {/* Variable Value */}
455
+ {isExpanded && (
456
+ <div className="p-3 bg-panel-bg border-t border-node-border">
457
+ <div className="flex items-start gap-2">
458
+ <div className="flex-1 overflow-x-auto">
459
+ <ValueRenderer
460
+ value={step.output}
461
+ onCopy={(key, val) => copyValue(key, val)}
462
+ copiedKey={copiedKey}
463
+ path={step.outputVariable || ''}
464
+ />
465
+ </div>
466
+ <button
467
+ onClick={() => copyValue(step.outputVariable || '', step.output)}
468
+ className="p-1.5 hover:bg-white/10 rounded transition-colors"
469
+ title="Copy entire value"
470
+ >
471
+ {copiedKey === step.outputVariable ? (
472
+ <Check className="w-4 h-4 text-success" />
473
+ ) : (
474
+ <Copy className="w-4 h-4 text-gray-400" />
475
+ )}
476
+ </button>
477
+ </div>
478
+ </div>
479
+ )}
480
+ </div>
481
+ );
482
+ })}
483
+ </div>
484
+ );
485
+ }
486
+
487
+ function ValueRenderer({
488
+ value,
489
+ onCopy,
490
+ copiedKey,
491
+ path,
492
+ depth = 0,
493
+ }: {
494
+ value: unknown;
495
+ onCopy: (key: string, value: unknown) => void;
496
+ copiedKey: string | null;
497
+ path: string;
498
+ depth?: number;
499
+ }) {
500
+ const [expanded, setExpanded] = useState(depth < 2);
501
+
502
+ if (value === null) {
503
+ return <span className="text-gray-500 font-mono text-xs">null</span>;
504
+ }
505
+
506
+ if (value === undefined) {
507
+ return <span className="text-gray-500 font-mono text-xs">undefined</span>;
508
+ }
509
+
510
+ if (typeof value === 'boolean') {
511
+ return (
512
+ <span className={`font-mono text-xs ${value ? 'text-success' : 'text-error'}`}>
513
+ {String(value)}
514
+ </span>
515
+ );
516
+ }
517
+
518
+ if (typeof value === 'number') {
519
+ return <span className="text-warning font-mono text-xs">{value}</span>;
520
+ }
521
+
522
+ if (typeof value === 'string') {
523
+ // Truncate long strings
524
+ const displayValue = value.length > 200 ? value.substring(0, 200) + '...' : value;
525
+ return (
526
+ <span className="text-success font-mono text-xs">
527
+ &quot;{displayValue}&quot;
528
+ </span>
529
+ );
530
+ }
531
+
532
+ if (Array.isArray(value)) {
533
+ if (value.length === 0) {
534
+ return <span className="text-gray-400 font-mono text-xs">[]</span>;
535
+ }
536
+
537
+ return (
538
+ <div className="space-y-1">
539
+ <button
540
+ onClick={() => setExpanded(!expanded)}
541
+ className="flex items-center gap-1 text-gray-400 hover:text-white transition-colors"
542
+ >
543
+ {expanded ? (
544
+ <ChevronDown className="w-3 h-3" />
545
+ ) : (
546
+ <ChevronRight className="w-3 h-3" />
547
+ )}
548
+ <span className="text-xs font-mono">Array({value.length})</span>
549
+ </button>
550
+ {expanded && (
551
+ <div className="ml-4 pl-2 border-l border-node-border space-y-1">
552
+ {value.slice(0, 20).map((item, index) => (
553
+ <div key={index} className="flex items-start gap-2">
554
+ <span className="text-gray-500 font-mono text-xs">[{index}]:</span>
555
+ <ValueRenderer
556
+ value={item}
557
+ onCopy={onCopy}
558
+ copiedKey={copiedKey}
559
+ path={`${path}[${index}]`}
560
+ depth={depth + 1}
561
+ />
562
+ </div>
563
+ ))}
564
+ {value.length > 20 && (
565
+ <div className="text-gray-500 text-xs">
566
+ ... and {value.length - 20} more items
567
+ </div>
568
+ )}
569
+ </div>
570
+ )}
571
+ </div>
572
+ );
573
+ }
574
+
575
+ if (typeof value === 'object') {
576
+ const entries = Object.entries(value);
577
+ if (entries.length === 0) {
578
+ return <span className="text-gray-400 font-mono text-xs">{'{}'}</span>;
579
+ }
580
+
581
+ return (
582
+ <div className="space-y-1">
583
+ <button
584
+ onClick={() => setExpanded(!expanded)}
585
+ className="flex items-center gap-1 text-gray-400 hover:text-white transition-colors"
586
+ >
587
+ {expanded ? (
588
+ <ChevronDown className="w-3 h-3" />
589
+ ) : (
590
+ <ChevronRight className="w-3 h-3" />
591
+ )}
592
+ <span className="text-xs font-mono">Object({entries.length} keys)</span>
593
+ </button>
594
+ {expanded && (
595
+ <div className="ml-4 pl-2 border-l border-node-border space-y-1">
596
+ {entries.slice(0, 30).map(([key, val]) => (
597
+ <div key={key} className="flex items-start gap-2">
598
+ <span className="text-primary font-mono text-xs">{key}:</span>
599
+ <ValueRenderer
600
+ value={val}
601
+ onCopy={onCopy}
602
+ copiedKey={copiedKey}
603
+ path={`${path}.${key}`}
604
+ depth={depth + 1}
605
+ />
606
+ </div>
607
+ ))}
608
+ {entries.length > 30 && (
609
+ <div className="text-gray-500 text-xs">
610
+ ... and {entries.length - 30} more keys
611
+ </div>
612
+ )}
613
+ </div>
614
+ )}
615
+ </div>
616
+ );
617
+ }
618
+
619
+ return <span className="text-gray-400 font-mono text-xs">{String(value)}</span>;
620
+ }
621
+
622
+ function getTypeLabel(value: unknown): string {
623
+ if (value === null) return 'null';
624
+ if (value === undefined) return 'undefined';
625
+ if (Array.isArray(value)) return `array[${value.length}]`;
626
+ if (typeof value === 'object') return `object`;
627
+ return typeof value;
628
+ }
629
+
630
+ function StatusIcon({ status }: { status: WorkflowStatus }) {
631
+ switch (status) {
632
+ case 'running':
633
+ return <Loader2 className="w-5 h-5 text-primary animate-spin" />;
634
+ case 'completed':
635
+ return <CheckCircle className="w-5 h-5 text-success" />;
636
+ case 'failed':
637
+ return <XCircle className="w-5 h-5 text-error" />;
638
+ case 'cancelled':
639
+ return <Square className="w-5 h-5 text-gray-400" />;
640
+ default:
641
+ return <div className="w-5 h-5 rounded-full bg-gray-500" />;
642
+ }
643
+ }
644
+
645
+ function StepStatusIcon({ status }: { status: StepStatus }) {
646
+ switch (status) {
647
+ case 'running':
648
+ return <Loader2 className="w-4 h-4 text-warning animate-spin" />;
649
+ case 'completed':
650
+ return <CheckCircle className="w-4 h-4 text-success" />;
651
+ case 'failed':
652
+ return <XCircle className="w-4 h-4 text-error" />;
653
+ case 'skipped':
654
+ return <SkipForward className="w-4 h-4 text-gray-400" />;
655
+ default:
656
+ return <div className="w-4 h-4 rounded-full border-2 border-gray-500" />;
657
+ }
658
+ }
659
+
660
+ function getStatusText(status: WorkflowStatus): string {
661
+ switch (status) {
662
+ case 'pending':
663
+ return 'Pending';
664
+ case 'running':
665
+ return 'Executing Workflow...';
666
+ case 'completed':
667
+ return 'Workflow Completed';
668
+ case 'failed':
669
+ return 'Workflow Failed';
670
+ case 'cancelled':
671
+ return 'Workflow Cancelled';
672
+ default:
673
+ return 'Unknown';
674
+ }
675
+ }
676
+
677
+ // Debug Panel Component
678
+ function DebugPanel({
679
+ debug,
680
+ onClearBreakpoints,
681
+ onAddWatchExpression,
682
+ onRemoveWatchExpression,
683
+ }: {
684
+ debug: DebugState;
685
+ onClearBreakpoints?: () => void;
686
+ onAddWatchExpression?: (expression: string) => void;
687
+ onRemoveWatchExpression?: (expression: string) => void;
688
+ }) {
689
+ const [newWatchExpr, setNewWatchExpr] = useState('');
690
+
691
+ const handleAddWatch = () => {
692
+ if (newWatchExpr.trim() && onAddWatchExpression) {
693
+ onAddWatchExpression(newWatchExpr.trim());
694
+ setNewWatchExpr('');
695
+ }
696
+ };
697
+
698
+ return (
699
+ <div className="space-y-4">
700
+ {/* Breakpoints Section */}
701
+ <div className="space-y-2">
702
+ <div className="flex items-center justify-between">
703
+ <h4 className="text-sm font-medium text-white flex items-center gap-2">
704
+ <Circle className="w-3 h-3 text-error" />
705
+ Breakpoints ({debug.breakpoints.size})
706
+ </h4>
707
+ {debug.breakpoints.size > 0 && onClearBreakpoints && (
708
+ <button
709
+ onClick={onClearBreakpoints}
710
+ className="text-xs text-gray-400 hover:text-white flex items-center gap-1"
711
+ >
712
+ <Trash2 className="w-3 h-3" />
713
+ Clear all
714
+ </button>
715
+ )}
716
+ </div>
717
+ <div className="bg-node-bg rounded-lg p-3">
718
+ {debug.breakpoints.size === 0 ? (
719
+ <div className="text-xs text-gray-500">
720
+ No breakpoints set. Click the dot next to a step to add one.
721
+ </div>
722
+ ) : (
723
+ <div className="space-y-1">
724
+ {Array.from(debug.breakpoints).map((stepId) => (
725
+ <div
726
+ key={stepId}
727
+ className="flex items-center gap-2 text-xs text-gray-300"
728
+ >
729
+ <Circle className="w-2 h-2 fill-current text-error" />
730
+ <span className="font-mono">{stepId}</span>
731
+ </div>
732
+ ))}
733
+ </div>
734
+ )}
735
+ </div>
736
+ </div>
737
+
738
+ {/* Call Stack Section */}
739
+ <div className="space-y-2">
740
+ <h4 className="text-sm font-medium text-white">Call Stack</h4>
741
+ <div className="bg-node-bg rounded-lg p-3">
742
+ {debug.callStack.length === 0 ? (
743
+ <div className="text-xs text-gray-500">No active call stack</div>
744
+ ) : (
745
+ <div className="space-y-1">
746
+ {debug.callStack.map((frame, index) => (
747
+ <div
748
+ key={index}
749
+ className={`flex items-center gap-2 text-xs ${
750
+ index === 0 ? 'text-primary' : 'text-gray-400'
751
+ }`}
752
+ >
753
+ <ArrowRight className="w-3 h-3" />
754
+ <span className="font-mono">{frame}</span>
755
+ </div>
756
+ ))}
757
+ </div>
758
+ )}
759
+ </div>
760
+ </div>
761
+
762
+ {/* Watch Expressions Section */}
763
+ <div className="space-y-2">
764
+ <h4 className="text-sm font-medium text-white">Watch Expressions</h4>
765
+ <div className="bg-node-bg rounded-lg p-3 space-y-2">
766
+ {/* Add new watch expression */}
767
+ {onAddWatchExpression && (
768
+ <div className="flex items-center gap-2">
769
+ <input
770
+ type="text"
771
+ value={newWatchExpr}
772
+ onChange={(e) => setNewWatchExpr(e.target.value)}
773
+ onKeyDown={(e) => e.key === 'Enter' && handleAddWatch()}
774
+ placeholder="Add expression..."
775
+ className="flex-1 bg-transparent border border-node-border rounded px-2 py-1 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary"
776
+ />
777
+ <button
778
+ onClick={handleAddWatch}
779
+ disabled={!newWatchExpr.trim()}
780
+ className="p-1 text-gray-400 hover:text-white disabled:opacity-50"
781
+ >
782
+ <Plus className="w-4 h-4" />
783
+ </button>
784
+ </div>
785
+ )}
786
+
787
+ {/* Watch list */}
788
+ {debug.watchExpressions.length === 0 ? (
789
+ <div className="text-xs text-gray-500">
790
+ No watch expressions. Add an expression to monitor its value.
791
+ </div>
792
+ ) : (
793
+ <div className="space-y-1">
794
+ {debug.watchExpressions.map((expr) => (
795
+ <div
796
+ key={expr}
797
+ className="flex items-center justify-between gap-2 text-xs group"
798
+ >
799
+ <div className="flex items-center gap-2 min-w-0">
800
+ <span className="text-primary font-mono truncate">{expr}</span>
801
+ <span className="text-gray-500">=</span>
802
+ <span className="text-gray-300 font-mono truncate">
803
+ (not evaluated)
804
+ </span>
805
+ </div>
806
+ {onRemoveWatchExpression && (
807
+ <button
808
+ onClick={() => onRemoveWatchExpression(expr)}
809
+ className="p-1 text-gray-500 hover:text-error opacity-0 group-hover:opacity-100 transition-opacity"
810
+ >
811
+ <X className="w-3 h-3" />
812
+ </button>
813
+ )}
814
+ </div>
815
+ ))}
816
+ </div>
817
+ )}
818
+ </div>
819
+ </div>
820
+
821
+ {/* Debug State Info */}
822
+ <div className="space-y-2">
823
+ <h4 className="text-sm font-medium text-white">Debug State</h4>
824
+ <div className="bg-node-bg rounded-lg p-3 text-xs space-y-1">
825
+ <div className="flex items-center justify-between">
826
+ <span className="text-gray-400">Current Step:</span>
827
+ <span className="text-white font-mono">
828
+ {debug.currentStepId || '(none)'}
829
+ </span>
830
+ </div>
831
+ <div className="flex items-center justify-between">
832
+ <span className="text-gray-400">Paused at Breakpoint:</span>
833
+ <span className={debug.pausedAtBreakpoint ? 'text-error' : 'text-gray-500'}>
834
+ {debug.pausedAtBreakpoint ? 'Yes' : 'No'}
835
+ </span>
836
+ </div>
837
+ <div className="flex items-center justify-between">
838
+ <span className="text-gray-400">Step Over Pending:</span>
839
+ <span className={debug.stepOverPending ? 'text-warning' : 'text-gray-500'}>
840
+ {debug.stepOverPending ? 'Yes' : 'No'}
841
+ </span>
842
+ </div>
843
+ </div>
844
+ </div>
845
+ </div>
846
+ );
847
+ }