@marktoflow/gui 2.0.0-alpha.1 → 2.0.0-alpha.12

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 (139) hide show
  1. package/.marktoflow/state/workflow-state.db +0 -0
  2. package/.marktoflow/state/workflow-state.db-shm +0 -0
  3. package/.marktoflow/state/workflow-state.db-wal +0 -0
  4. package/.turbo/turbo-build.log +24 -8
  5. package/.turbo/turbo-test.log +29 -13
  6. package/README.md +49 -3
  7. package/dist/client/assets/index-CM44OayM.js +704 -0
  8. package/dist/client/assets/index-CM44OayM.js.map +1 -0
  9. package/dist/client/assets/index-Dru63gi6.css +1 -0
  10. package/dist/client/index.html +2 -2
  11. package/dist/server/index.js +93 -33
  12. package/dist/server/index.js.map +1 -1
  13. package/dist/server/routes/ai.js +38 -1
  14. package/dist/server/routes/ai.js.map +1 -1
  15. package/dist/server/routes/execute.js +23 -22
  16. package/dist/server/routes/execute.js.map +1 -1
  17. package/dist/server/routes/executions.js +125 -0
  18. package/dist/server/routes/executions.js.map +1 -0
  19. package/dist/server/{server/routes → routes}/tools.js +406 -0
  20. package/dist/server/{server/routes → routes}/tools.js.map +1 -1
  21. package/dist/server/routes/workflows.js +41 -5
  22. package/dist/server/routes/workflows.js.map +1 -1
  23. package/dist/server/services/AIService.js +55 -202
  24. package/dist/server/services/AIService.js.map +1 -1
  25. package/dist/server/services/FileWatcher.js +0 -2
  26. package/dist/server/services/FileWatcher.js.map +1 -1
  27. package/dist/server/services/WorkflowService.js +199 -16
  28. package/dist/server/services/WorkflowService.js.map +1 -1
  29. package/dist/server/services/agents/codex-provider.js +270 -0
  30. package/dist/server/services/agents/codex-provider.js.map +1 -0
  31. package/dist/server/{server/services → services}/agents/prompts.js +27 -0
  32. package/dist/server/services/agents/prompts.js.map +1 -0
  33. package/dist/server/{server/services → services}/agents/registry.js +20 -0
  34. package/dist/server/services/agents/registry.js.map +1 -0
  35. package/dist/server/websocket/index.js +12 -0
  36. package/dist/server/websocket/index.js.map +1 -1
  37. package/marktoflow-gui-2.0.0-alpha.12.tgz +0 -0
  38. package/package.json +19 -5
  39. package/scripts/flatten-dist.js +69 -0
  40. package/src/client/components/Canvas/Canvas.tsx +27 -7
  41. package/src/client/components/Canvas/ExecutionOverlay.tsx +120 -32
  42. package/src/client/components/Canvas/ForEachNode.tsx +152 -0
  43. package/src/client/components/Canvas/IfElseNode.tsx +141 -0
  44. package/src/client/components/Canvas/NodeContextMenu.tsx +8 -4
  45. package/src/client/components/Canvas/ParallelNode.tsx +157 -0
  46. package/src/client/components/Canvas/SwitchNode.tsx +185 -0
  47. package/src/client/components/Canvas/Toolbar.tsx +59 -21
  48. package/src/client/components/Canvas/TransformNode.tsx +194 -0
  49. package/src/client/components/Canvas/TryCatchNode.tsx +164 -0
  50. package/src/client/components/Canvas/WhileNode.tsx +161 -0
  51. package/src/client/components/Canvas/index.ts +24 -0
  52. package/src/client/components/Debug/VariableInspector.tsx +148 -0
  53. package/src/client/components/Prompt/PromptInput.tsx +3 -1
  54. package/src/client/components/Settings/ProviderSwitcher.tsx +228 -0
  55. package/src/client/components/Sidebar/ImportDialog.tsx +257 -0
  56. package/src/client/components/Sidebar/Sidebar.tsx +21 -2
  57. package/src/client/components/common/KeyboardShortcuts.tsx +8 -2
  58. package/src/client/stores/agentStore.ts +109 -0
  59. package/src/client/stores/executionStore.ts +64 -2
  60. package/src/client/stores/workflowStore.ts +10 -2
  61. package/src/client/styles/globals.css +106 -0
  62. package/src/client/utils/platform.ts +46 -0
  63. package/src/client/utils/serviceIcons.tsx +33 -0
  64. package/src/client/utils/workflowToGraph.ts +245 -21
  65. package/src/server/index.ts +25 -2
  66. package/src/server/routes/executions.ts +136 -0
  67. package/src/server/routes/tools.ts +406 -0
  68. package/src/server/routes/workflows.ts +42 -1
  69. package/src/server/services/WorkflowService.ts +176 -16
  70. package/src/server/services/agents/codex-provider.ts +398 -0
  71. package/src/server/services/agents/prompts.ts +27 -0
  72. package/src/server/services/agents/registry.ts +21 -0
  73. package/src/server/websocket/index.ts +13 -0
  74. package/tailwind.config.ts +1 -1
  75. package/tests/integration/api.test.ts +203 -1
  76. package/tests/integration/testApp.ts +1 -1
  77. package/tests/setup.ts +35 -0
  78. package/tests/unit/ForEachNode.test.tsx +308 -0
  79. package/tests/unit/IfElseNode.test.tsx +235 -0
  80. package/tests/unit/ParallelNode.test.tsx +344 -0
  81. package/tests/unit/SwitchNode.test.tsx +327 -0
  82. package/tests/unit/TransformNode.test.tsx +386 -0
  83. package/tests/unit/TryCatchNode.test.tsx +243 -0
  84. package/tests/unit/WhileNode.test.tsx +230 -0
  85. package/tests/unit/agentStore.test.ts +218 -0
  86. package/tests/unit/codexProvider.test.ts +399 -0
  87. package/tests/unit/executionStore.test.ts +40 -0
  88. package/tests/unit/platform.test.ts +118 -0
  89. package/tests/unit/serviceIcons.test.ts +197 -0
  90. package/tests/unit/workflowToGraph.test.ts +22 -0
  91. package/dist/client/assets/index-DwTI8opO.js +0 -608
  92. package/dist/client/assets/index-DwTI8opO.js.map +0 -1
  93. package/dist/client/assets/index-RoEdL6gO.css +0 -1
  94. package/dist/server/index.d.ts +0 -3
  95. package/dist/server/index.d.ts.map +0 -1
  96. package/dist/server/server/index.js +0 -95
  97. package/dist/server/server/index.js.map +0 -1
  98. package/dist/server/server/routes/ai.js +0 -87
  99. package/dist/server/server/routes/ai.js.map +0 -1
  100. package/dist/server/server/routes/execute.js +0 -63
  101. package/dist/server/server/routes/execute.js.map +0 -1
  102. package/dist/server/server/routes/workflows.js +0 -99
  103. package/dist/server/server/routes/workflows.js.map +0 -1
  104. package/dist/server/server/services/AIService.js +0 -69
  105. package/dist/server/server/services/AIService.js.map +0 -1
  106. package/dist/server/server/services/FileWatcher.js +0 -60
  107. package/dist/server/server/services/FileWatcher.js.map +0 -1
  108. package/dist/server/server/services/WorkflowService.js +0 -363
  109. package/dist/server/server/services/WorkflowService.js.map +0 -1
  110. package/dist/server/server/services/agents/prompts.js.map +0 -1
  111. package/dist/server/server/services/agents/registry.js.map +0 -1
  112. package/dist/server/server/websocket/index.js +0 -85
  113. package/dist/server/server/websocket/index.js.map +0 -1
  114. package/dist/server/services/AIService.d.ts +0 -30
  115. package/dist/server/services/AIService.d.ts.map +0 -1
  116. package/dist/server/services/FileWatcher.d.ts +0 -10
  117. package/dist/server/services/FileWatcher.d.ts.map +0 -1
  118. package/dist/server/services/WorkflowService.d.ts +0 -54
  119. package/dist/server/services/WorkflowService.d.ts.map +0 -1
  120. package/dist/server/websocket/index.d.ts +0 -10
  121. package/dist/server/websocket/index.d.ts.map +0 -1
  122. /package/dist/server/{server/services → services}/agents/claude-code-provider.js +0 -0
  123. /package/dist/server/{server/services → services}/agents/claude-code-provider.js.map +0 -0
  124. /package/dist/server/{server/services → services}/agents/claude-provider.js +0 -0
  125. /package/dist/server/{server/services → services}/agents/claude-provider.js.map +0 -0
  126. /package/dist/server/{server/services → services}/agents/copilot-provider.js +0 -0
  127. /package/dist/server/{server/services → services}/agents/copilot-provider.js.map +0 -0
  128. /package/dist/server/{server/services → services}/agents/demo-provider.js +0 -0
  129. /package/dist/server/{server/services → services}/agents/demo-provider.js.map +0 -0
  130. /package/dist/server/{server/services → services}/agents/index.js +0 -0
  131. /package/dist/server/{server/services → services}/agents/index.js.map +0 -0
  132. /package/dist/server/{server/services → services}/agents/ollama-provider.js +0 -0
  133. /package/dist/server/{server/services → services}/agents/ollama-provider.js.map +0 -0
  134. /package/dist/server/{server/services → services}/agents/types.js +0 -0
  135. /package/dist/server/{server/services → services}/agents/types.js.map +0 -0
  136. /package/dist/{server/shared → shared}/constants.js +0 -0
  137. /package/dist/{server/shared → shared}/constants.js.map +0 -0
  138. /package/dist/{server/shared → shared}/types.js +0 -0
  139. /package/dist/{server/shared → shared}/types.js.map +0 -0
@@ -0,0 +1,157 @@
1
+ import { memo } from 'react';
2
+ import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
3
+ import { Layers, CheckCircle, XCircle, Clock, AlertTriangle } from 'lucide-react';
4
+
5
+ export interface ParallelNodeData extends Record<string, unknown> {
6
+ id: string;
7
+ name?: string;
8
+ branches: Array<{ id: string; name?: string }>;
9
+ maxConcurrent?: number;
10
+ onError?: 'stop' | 'continue';
11
+ status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
12
+ activeBranches?: string[];
13
+ completedBranches?: string[];
14
+ failedBranches?: string[];
15
+ maxConcurrentExceeded?: boolean;
16
+ }
17
+
18
+ export type ParallelNodeType = Node<ParallelNodeData, 'parallel'>;
19
+
20
+ function ParallelNodeComponent({ data, selected }: NodeProps<ParallelNodeType>) {
21
+ const statusConfig: Record<
22
+ NonNullable<ParallelNodeData['status']>,
23
+ { icon: typeof Clock; color: string; bgColor: string; animate?: boolean }
24
+ > = {
25
+ pending: { icon: Clock, color: 'text-gray-400', bgColor: 'bg-gray-400/10' },
26
+ running: {
27
+ icon: Layers,
28
+ color: 'text-green-400',
29
+ bgColor: 'bg-green-400/10',
30
+ animate: true,
31
+ },
32
+ completed: {
33
+ icon: CheckCircle,
34
+ color: 'text-success',
35
+ bgColor: 'bg-success/10',
36
+ },
37
+ failed: { icon: XCircle, color: 'text-error', bgColor: 'bg-error/10' },
38
+ skipped: {
39
+ icon: XCircle,
40
+ color: 'text-gray-500',
41
+ bgColor: 'bg-gray-500/10',
42
+ },
43
+ };
44
+
45
+ const status = data.status || 'pending';
46
+ const config = statusConfig[status];
47
+ const StatusIcon = config.icon;
48
+
49
+ return (
50
+ <div
51
+ className={`control-flow-node parallel-node p-0 ${selected ? 'selected' : ''} ${status === 'running' ? 'running' : ''} ${status === 'completed' ? 'completed' : ''} ${status === 'failed' ? 'failed' : ''}`}
52
+ style={{
53
+ background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
54
+ }}
55
+ >
56
+ {/* Input handle */}
57
+ <Handle
58
+ type="target"
59
+ position={Position.Top}
60
+ className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
61
+ />
62
+
63
+ {/* Node header */}
64
+ <div className="flex items-center gap-3 p-3 border-b border-white/20">
65
+ <div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
66
+ <Layers className="w-5 h-5 text-white" />
67
+ </div>
68
+ <div className="flex-1 min-w-0">
69
+ <div className="text-sm font-medium text-white">
70
+ {data.name || 'Parallel'}
71
+ </div>
72
+ <div className="text-xs text-white/70">Concurrent Execution</div>
73
+ </div>
74
+ <div
75
+ className={`w-6 h-6 rounded-full ${config.bgColor} flex items-center justify-center`}
76
+ >
77
+ <StatusIcon
78
+ className={`w-4 h-4 ${config.color} ${config.animate ? 'animate-pulse' : ''}`}
79
+ />
80
+ </div>
81
+ </div>
82
+
83
+ {/* Node body */}
84
+ <div className="p-3 bg-white/10">
85
+ {/* Max concurrent warning */}
86
+ {data.maxConcurrentExceeded && (
87
+ <div className="mb-3 p-2 bg-yellow-500/20 border border-yellow-500/30 rounded flex items-center gap-2">
88
+ <AlertTriangle className="w-4 h-4 text-yellow-200" />
89
+ <span className="text-xs text-yellow-200 font-medium">
90
+ Rate limiting active
91
+ </span>
92
+ </div>
93
+ )}
94
+
95
+ <div className="text-xs text-white/90 mb-3">
96
+ <span className="text-white/60">Branches:</span>{' '}
97
+ <span className="font-medium">{data.branches?.length || 0}</span>
98
+ {data.maxConcurrent && (
99
+ <>
100
+ {' '}
101
+ <span className="text-white/60">• Max Concurrent:</span>{' '}
102
+ <span className={`font-medium ${data.maxConcurrentExceeded ? 'text-yellow-300' : ''}`}>
103
+ {data.maxConcurrent}
104
+ </span>
105
+ </>
106
+ )}
107
+ </div>
108
+
109
+ {/* Branch indicators */}
110
+ <div className="flex flex-wrap gap-2 mb-3">
111
+ {data.branches?.slice(0, 6).map((branch) => {
112
+ const isActive = data.activeBranches?.includes(branch.id);
113
+ const isCompleted = data.completedBranches?.includes(branch.id);
114
+ const isFailed = data.failedBranches?.includes(branch.id);
115
+ return (
116
+ <div
117
+ key={branch.id}
118
+ className={`px-2 py-1 rounded text-xs font-medium transition-colors ${
119
+ isFailed
120
+ ? 'bg-red-500/30 text-red-200'
121
+ : isCompleted
122
+ ? 'bg-green-500/30 text-green-200'
123
+ : isActive
124
+ ? 'bg-blue-500/30 text-blue-200 animate-pulse'
125
+ : 'bg-white/10 text-white/60'
126
+ }`}
127
+ title={branch.name || branch.id}
128
+ >
129
+ {branch.name || `B${branch.id.slice(-2)}`}
130
+ </div>
131
+ );
132
+ })}
133
+ {data.branches && data.branches.length > 6 && (
134
+ <div className="px-2 py-1 rounded text-xs font-medium bg-white/10 text-white/60">
135
+ +{data.branches.length - 6}
136
+ </div>
137
+ )}
138
+ </div>
139
+
140
+ {/* Error handling */}
141
+ <div className="text-xs text-white/50 flex items-center gap-1">
142
+ <span>On Error:</span>
143
+ <span className="font-medium">{data.onError || 'stop'}</span>
144
+ </div>
145
+ </div>
146
+
147
+ {/* Output handle */}
148
+ <Handle
149
+ type="source"
150
+ position={Position.Bottom}
151
+ className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
152
+ />
153
+ </div>
154
+ );
155
+ }
156
+
157
+ export const ParallelNode = memo(ParallelNodeComponent);
@@ -0,0 +1,185 @@
1
+ import { memo } from 'react';
2
+ import { Handle, Position, type Node, type NodeProps } from '@xyflow/react';
3
+ import { GitFork, CheckCircle, XCircle, Clock } from 'lucide-react';
4
+
5
+ export interface SwitchNodeData extends Record<string, unknown> {
6
+ id: string;
7
+ name?: string;
8
+ expression: string;
9
+ cases: Record<string, unknown>;
10
+ hasDefault?: boolean;
11
+ status?: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
12
+ activeCase?: string | null;
13
+ skippedBranches?: string[];
14
+ }
15
+
16
+ export type SwitchNodeType = Node<SwitchNodeData, 'switch'>;
17
+
18
+ function SwitchNodeComponent({ data, selected }: NodeProps<SwitchNodeType>) {
19
+ const statusConfig: Record<
20
+ NonNullable<SwitchNodeData['status']>,
21
+ { icon: typeof Clock; color: string; bgColor: string; animate?: boolean }
22
+ > = {
23
+ pending: { icon: Clock, color: 'text-gray-400', bgColor: 'bg-gray-400/10' },
24
+ running: {
25
+ icon: GitFork,
26
+ color: 'text-purple-400',
27
+ bgColor: 'bg-purple-400/10',
28
+ animate: true,
29
+ },
30
+ completed: {
31
+ icon: CheckCircle,
32
+ color: 'text-success',
33
+ bgColor: 'bg-success/10',
34
+ },
35
+ failed: { icon: XCircle, color: 'text-error', bgColor: 'bg-error/10' },
36
+ skipped: {
37
+ icon: XCircle,
38
+ color: 'text-gray-500',
39
+ bgColor: 'bg-gray-500/10',
40
+ },
41
+ };
42
+
43
+ const status = data.status || 'pending';
44
+ const config = statusConfig[status];
45
+ const StatusIcon = config.icon;
46
+
47
+ const caseKeys = Object.keys(data.cases || {});
48
+ const displayCases = caseKeys.slice(0, 4); // Show up to 4 cases
49
+ const hasMore = caseKeys.length > 4;
50
+
51
+ // Calculate handle positions to avoid overlap
52
+ const totalHandles = Math.min(caseKeys.length, 4) + (data.hasDefault ? 1 : 0);
53
+ const getHandlePosition = (index: number) => {
54
+ if (totalHandles === 1) return 50;
55
+ return ((index + 1) / (totalHandles + 1)) * 100;
56
+ };
57
+
58
+ return (
59
+ <div
60
+ className={`control-flow-node switch-node p-0 ${selected ? 'selected' : ''} ${status === 'running' ? 'running' : ''} ${status === 'completed' ? 'completed' : ''} ${status === 'failed' ? 'failed' : ''}`}
61
+ style={{
62
+ background: 'linear-gradient(135deg, #a855f7 0%, #ec4899 100%)',
63
+ }}
64
+ >
65
+ {/* Input handle */}
66
+ <Handle
67
+ type="target"
68
+ position={Position.Top}
69
+ className="!w-3 !h-3 !bg-primary !border-2 !border-node-bg"
70
+ />
71
+
72
+ {/* Node header */}
73
+ <div className="flex items-center gap-3 p-3 border-b border-white/20">
74
+ <div className="w-8 h-8 rounded-lg bg-white/20 flex items-center justify-center">
75
+ <GitFork className="w-5 h-5 text-white" />
76
+ </div>
77
+ <div className="flex-1 min-w-0">
78
+ <div className="text-sm font-medium text-white">
79
+ {data.name || 'Switch'}
80
+ </div>
81
+ <div className="text-xs text-white/70">Multi-Branch Router</div>
82
+ </div>
83
+ <div
84
+ className={`w-6 h-6 rounded-full ${config.bgColor} flex items-center justify-center`}
85
+ >
86
+ <StatusIcon
87
+ className={`w-4 h-4 ${config.color} ${config.animate ? 'animate-pulse' : ''}`}
88
+ />
89
+ </div>
90
+ </div>
91
+
92
+ {/* Node body */}
93
+ <div className="p-3 bg-white/10">
94
+ <div className="text-xs text-white/90 mb-3">
95
+ <span className="text-white/60">Expression:</span>{' '}
96
+ <span className="font-mono">{data.expression || 'Not set'}</span>
97
+ </div>
98
+
99
+ {/* Case list */}
100
+ <div className="space-y-2">
101
+ <div className="text-xs text-white/70 font-medium mb-1">Cases:</div>
102
+ {displayCases.map((caseKey, index) => {
103
+ const isActive = data.activeCase === caseKey;
104
+ const isSkipped = data.skippedBranches?.includes(caseKey);
105
+ const handlePosition = getHandlePosition(index);
106
+
107
+ return (
108
+ <div key={caseKey} className="relative">
109
+ <div
110
+ className={`text-xs px-2 py-1.5 rounded font-medium transition-colors relative ${
111
+ isActive
112
+ ? 'bg-purple-500/30 text-purple-200 ring-1 ring-purple-400/50'
113
+ : isSkipped
114
+ ? 'bg-gray-500/20 text-gray-400 line-through'
115
+ : 'bg-white/5 text-white/70'
116
+ }`}
117
+ >
118
+ {caseKey}
119
+ {isSkipped && (
120
+ <span className="ml-2 text-[8px] px-1 py-0.5 rounded bg-gray-500/30">
121
+ SKIPPED
122
+ </span>
123
+ )}
124
+ </div>
125
+ </div>
126
+ );
127
+ })}
128
+
129
+ {hasMore && (
130
+ <div className="text-xs px-2 py-1 rounded bg-white/5 text-white/60 text-center">
131
+ +{caseKeys.length - 4} more cases
132
+ </div>
133
+ )}
134
+
135
+ {/* Default case */}
136
+ {data.hasDefault && (
137
+ <div className="relative">
138
+ <div
139
+ className={`text-xs px-2 py-1.5 rounded font-medium transition-colors ${
140
+ data.activeCase === 'default'
141
+ ? 'bg-gray-500/30 text-gray-200 ring-1 ring-gray-400/50'
142
+ : 'bg-white/5 text-white/70'
143
+ }`}
144
+ >
145
+ default
146
+ </div>
147
+ </div>
148
+ )}
149
+ </div>
150
+
151
+ {/* Case count */}
152
+ <div className="mt-3 text-xs text-white/50 flex items-center gap-2">
153
+ <span>ℹ️</span>
154
+ <span>
155
+ {caseKeys.length} case{caseKeys.length !== 1 ? 's' : ''}
156
+ {data.hasDefault ? ' + default' : ''}
157
+ </span>
158
+ </div>
159
+ </div>
160
+
161
+ {/* Output handles - positioned independently to avoid overlap */}
162
+ {displayCases.map((caseKey, index) => (
163
+ <Handle
164
+ key={`handle-${caseKey}`}
165
+ type="source"
166
+ position={Position.Bottom}
167
+ id={`case-${caseKey}`}
168
+ style={{ left: `${getHandlePosition(index)}%` }}
169
+ className="!w-2.5 !h-2.5 !bg-purple-400 !border-2 !border-node-bg"
170
+ />
171
+ ))}
172
+ {data.hasDefault && (
173
+ <Handle
174
+ type="source"
175
+ position={Position.Bottom}
176
+ id="case-default"
177
+ style={{ left: `${getHandlePosition(displayCases.length)}%` }}
178
+ className="!w-2.5 !h-2.5 !bg-gray-400 !border-2 !border-node-bg"
179
+ />
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ export const SwitchNode = memo(SwitchNodeComponent);
@@ -1,3 +1,4 @@
1
+ import { useState, useEffect } from 'react';
1
2
  import {
2
3
  Plus,
3
4
  Play,
@@ -11,10 +12,15 @@ import {
11
12
  Redo,
12
13
  Copy,
13
14
  Trash2,
15
+ Bot,
16
+ ChevronDown,
14
17
  } from 'lucide-react';
15
18
  import { useCanvas } from '../../hooks/useCanvas';
16
19
  import { useEditorStore } from '../../stores/editorStore';
17
20
  import { useReactFlow } from '@xyflow/react';
21
+ import { getModKey } from '../../utils/platform';
22
+ import { useAgentStore } from '../../stores/agentStore';
23
+ import { ProviderSwitcher } from '../Settings/ProviderSwitcher';
18
24
 
19
25
  interface ToolbarProps {
20
26
  onAddStep: () => void;
@@ -33,11 +39,21 @@ export function Toolbar({
33
39
  useCanvas();
34
40
  const { undo, redo, undoStack, redoStack } = useEditorStore();
35
41
  const { zoomIn, zoomOut } = useReactFlow();
42
+ const modKey = getModKey();
43
+ const { providers, activeProviderId, loadProviders } = useAgentStore();
44
+ const [showProviderSwitcher, setShowProviderSwitcher] = useState(false);
36
45
 
37
46
  const canUndo = undoStack.length > 0;
38
47
  const canRedo = redoStack.length > 0;
39
48
  const hasSelection = selectedNodes.length > 0;
40
49
 
50
+ // Load providers on mount
51
+ useEffect(() => {
52
+ loadProviders();
53
+ }, [loadProviders]);
54
+
55
+ const activeProvider = providers.find((p) => p.id === activeProviderId);
56
+
41
57
  return (
42
58
  <div className="absolute top-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1 px-2 py-1.5 bg-panel-bg/95 backdrop-blur border border-node-border rounded-lg shadow-lg">
43
59
  {/* Add Step */}
@@ -56,14 +72,14 @@ export function Toolbar({
56
72
  label="Undo"
57
73
  onClick={() => undo()}
58
74
  disabled={!canUndo}
59
- shortcut="⌘Z"
75
+ shortcut={`${modKey}Z`}
60
76
  />
61
77
  <ToolbarButton
62
78
  icon={<Redo className="w-4 h-4" />}
63
79
  label="Redo"
64
80
  onClick={() => redo()}
65
81
  disabled={!canRedo}
66
- shortcut="⌘⇧Z"
82
+ shortcut={`${modKey}⇧Z`}
67
83
  />
68
84
 
69
85
  <ToolbarDivider />
@@ -74,7 +90,7 @@ export function Toolbar({
74
90
  label="Duplicate"
75
91
  onClick={duplicateSelected}
76
92
  disabled={!hasSelection}
77
- shortcut="⌘D"
93
+ shortcut={`${modKey}D`}
78
94
  />
79
95
  <ToolbarButton
80
96
  icon={<Trash2 className="w-4 h-4" />}
@@ -91,44 +107,60 @@ export function Toolbar({
91
107
  icon={<Layout className="w-4 h-4" />}
92
108
  label="Auto Layout"
93
109
  onClick={autoLayout}
94
- shortcut="⌘L"
110
+ shortcut={`${modKey}L`}
95
111
  />
96
112
  <ToolbarButton
97
113
  icon={<ZoomIn className="w-4 h-4" />}
98
114
  label="Zoom In"
99
115
  onClick={() => zoomIn()}
100
- shortcut="⌘+"
116
+ shortcut={`${modKey}+`}
101
117
  />
102
118
  <ToolbarButton
103
119
  icon={<ZoomOut className="w-4 h-4" />}
104
120
  label="Zoom Out"
105
121
  onClick={() => zoomOut()}
106
- shortcut="⌘-"
122
+ shortcut={`${modKey}-`}
107
123
  />
108
124
  <ToolbarButton
109
125
  icon={<Maximize className="w-4 h-4" />}
110
126
  label="Fit View"
111
127
  onClick={fitView}
112
- shortcut="⌘0"
128
+ shortcut={`${modKey}0`}
113
129
  />
114
130
 
115
131
  <ToolbarDivider />
116
132
 
133
+ {/* AI Provider */}
134
+ <button
135
+ onClick={() => setShowProviderSwitcher(true)}
136
+ className="flex items-center gap-1.5 px-2 py-1.5 rounded text-sm text-gray-300 hover:text-white hover:bg-white/10 transition-colors"
137
+ title="Select AI Provider"
138
+ >
139
+ <Bot className="w-4 h-4" />
140
+ <span className="hidden sm:inline text-xs">
141
+ {activeProvider?.name || 'No Provider'}
142
+ </span>
143
+ <ChevronDown className="w-3 h-3" />
144
+ </button>
145
+
117
146
  {/* Execute */}
118
147
  {onExecute && (
119
- <ToolbarButton
120
- icon={
121
- isExecuting ? (
122
- <Pause className="w-4 h-4" />
123
- ) : (
124
- <Play className="w-4 h-4" />
125
- )
126
- }
127
- label={isExecuting ? 'Stop' : 'Execute'}
128
- onClick={onExecute}
129
- variant={isExecuting ? 'destructive' : 'primary'}
130
- shortcut="⌘⏎"
131
- />
148
+ <>
149
+ <ToolbarDivider />
150
+ <ToolbarButton
151
+ icon={
152
+ isExecuting ? (
153
+ <Pause className="w-4 h-4" />
154
+ ) : (
155
+ <Play className="w-4 h-4" />
156
+ )
157
+ }
158
+ label={isExecuting ? 'Stop' : 'Execute'}
159
+ onClick={onExecute}
160
+ variant={isExecuting ? 'destructive' : 'primary'}
161
+ shortcut={`${modKey}⏎`}
162
+ />
163
+ </>
132
164
  )}
133
165
 
134
166
  {/* Save */}
@@ -137,9 +169,15 @@ export function Toolbar({
137
169
  icon={<Save className="w-4 h-4" />}
138
170
  label="Save"
139
171
  onClick={onSave}
140
- shortcut="⌘S"
172
+ shortcut={`${modKey}S`}
141
173
  />
142
174
  )}
175
+
176
+ {/* Provider Switcher Modal */}
177
+ <ProviderSwitcher
178
+ open={showProviderSwitcher}
179
+ onOpenChange={setShowProviderSwitcher}
180
+ />
143
181
  </div>
144
182
  );
145
183
  }