@swarmclawai/swarmclaw 1.3.6 → 1.4.0

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 (96) hide show
  1. package/README.md +32 -1
  2. package/package.json +9 -3
  3. package/src/.env.local +4 -0
  4. package/src/app/api/.well-known/agent-card/route.ts +46 -0
  5. package/src/app/api/a2a/route.ts +56 -0
  6. package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
  7. package/src/app/api/chats/[id]/deploy/route.ts +2 -2
  8. package/src/app/api/openclaw/sync/route.ts +1 -1
  9. package/src/app/api/swarmfeed/channels/route.ts +14 -0
  10. package/src/app/api/swarmfeed/posts/route.ts +60 -0
  11. package/src/app/api/swarmfeed/route.ts +37 -0
  12. package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
  13. package/src/app/protocols/page.tsx +16 -7
  14. package/src/app/swarmfeed/page.tsx +7 -0
  15. package/src/cli/index.js +19 -0
  16. package/src/cli/spec.js +8 -0
  17. package/src/components/agents/agent-avatar.tsx +2 -5
  18. package/src/components/agents/agent-sheet.tsx +10 -0
  19. package/src/components/auth/access-key-gate.tsx +25 -0
  20. package/src/components/layout/sidebar-rail.tsx +52 -0
  21. package/src/components/protocols/builder/edge-editor.tsx +43 -0
  22. package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
  23. package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
  24. package/src/components/protocols/builder/edge-types/index.ts +3 -0
  25. package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
  26. package/src/components/protocols/builder/node-inspector.tsx +227 -0
  27. package/src/components/protocols/builder/node-palette.tsx +97 -0
  28. package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
  29. package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
  30. package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
  31. package/src/components/protocols/builder/node-types/index.ts +9 -0
  32. package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
  33. package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
  34. package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
  35. package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
  36. package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
  37. package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
  38. package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
  39. package/src/components/protocols/builder/run-overlay.tsx +29 -0
  40. package/src/components/protocols/builder/template-gallery.tsx +53 -0
  41. package/src/components/protocols/builder/validation-panel.tsx +57 -0
  42. package/src/components/skills/skills-workspace.tsx +1 -9
  43. package/src/features/protocols/builder/hooks/index.ts +2 -0
  44. package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
  45. package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
  46. package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
  47. package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
  48. package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
  49. package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
  50. package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
  51. package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
  52. package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
  53. package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
  54. package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
  55. package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
  56. package/src/features/swarmfeed/compose-post.tsx +139 -0
  57. package/src/features/swarmfeed/feed-page.tsx +136 -0
  58. package/src/features/swarmfeed/post-card.tsx +114 -0
  59. package/src/features/swarmfeed/queries.ts +28 -0
  60. package/src/lib/a2a/agent-card.ts +61 -0
  61. package/src/lib/a2a/auth.ts +54 -0
  62. package/src/lib/a2a/client.ts +133 -0
  63. package/src/lib/a2a/discovery.ts +116 -0
  64. package/src/lib/a2a/handlers.ts +176 -0
  65. package/src/lib/a2a/json-rpc-router.ts +38 -0
  66. package/src/lib/a2a/types.ts +95 -0
  67. package/src/lib/app/navigation.ts +1 -0
  68. package/src/lib/app/view-constants.ts +9 -1
  69. package/src/lib/providers/anthropic.ts +111 -107
  70. package/src/lib/providers/openai.ts +146 -142
  71. package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
  72. package/src/lib/server/agents/main-agent-loop.ts +377 -41
  73. package/src/lib/server/chat-execution/chat-execution.ts +12 -7
  74. package/src/lib/server/extensions.ts +11 -0
  75. package/src/lib/server/openclaw/sync.ts +4 -4
  76. package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
  77. package/src/lib/server/protocols/protocol-normalization.ts +1 -0
  78. package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
  79. package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
  80. package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
  81. package/src/lib/server/protocols/protocol-types.ts +1 -0
  82. package/src/lib/server/session-tools/delegate.ts +151 -77
  83. package/src/lib/server/storage-auth.ts +10 -2
  84. package/src/lib/server/storage-normalization.ts +11 -0
  85. package/src/lib/server/storage.ts +100 -0
  86. package/src/lib/server/working-state/service.test.ts +2 -3
  87. package/src/lib/server/working-state/service.ts +37 -6
  88. package/src/lib/swarmfeed-client.ts +157 -0
  89. package/src/lib/validation/schemas.ts +1 -1
  90. package/src/stores/slices/data-slice.ts +3 -0
  91. package/src/stores/use-approval-store.ts +4 -1
  92. package/src/types/agent.ts +31 -1
  93. package/src/types/index.ts +1 -0
  94. package/src/types/protocol.ts +19 -0
  95. package/src/types/session.ts +1 -1
  96. package/src/types/swarmfeed.ts +30 -0
@@ -9,6 +9,11 @@ interface AccessKeyGateProps {
9
9
  }
10
10
 
11
11
  const AUTH_CHECK_TIMEOUT_MS = 8_000
12
+ const NETWORK_LINKS = [
13
+ { href: 'https://www.swarmdock.ai', label: 'SwarmDock' },
14
+ { href: 'https://swarmrecall.ai', label: 'SwarmRecall' },
15
+ { href: 'https://swarmrelay.ai', label: 'SwarmRelay' },
16
+ ]
12
17
 
13
18
  function isExpectedAuthCheckError(err: unknown): boolean {
14
19
  return isAbortError(err) || isTimeoutError(err)
@@ -421,6 +426,26 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
421
426
  </form>
422
427
  </>
423
428
  )}
429
+
430
+ <div className="mt-10 border-t border-white/[0.06] pt-5" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
431
+ <p className="text-[10px] font-700 uppercase tracking-[0.18em] text-text-3/55">
432
+ Network
433
+ </p>
434
+ <div className="mt-3 flex flex-wrap items-center justify-center gap-2.5">
435
+ {NETWORK_LINKS.map((link) => (
436
+ <a
437
+ key={link.href}
438
+ href={link.href}
439
+ target="_blank"
440
+ rel="noopener noreferrer"
441
+ className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1.5 text-[12px] text-text-3
442
+ no-underline transition-all duration-200 hover:border-white/[0.14] hover:bg-white/[0.06] hover:text-text"
443
+ >
444
+ {link.label}
445
+ </a>
446
+ ))}
447
+ </div>
448
+ </div>
424
449
  </div>
425
450
  </div>
426
451
  )
@@ -16,6 +16,11 @@ import type { AppView } from '@/types'
16
16
 
17
17
  const RAIL_EXPANDED_KEY = 'sc_rail_expanded'
18
18
  const GITHUB_REPO_URL = 'https://github.com/swarmclawai/swarmclaw'
19
+ const NETWORK_LINKS = [
20
+ { href: 'https://www.swarmdock.ai', label: 'SwarmDock', abbr: 'DO' },
21
+ { href: 'https://swarmrecall.ai', label: 'SwarmRecall', abbr: 'RE' },
22
+ { href: 'https://swarmrelay.ai', label: 'SwarmRelay', abbr: 'RL' },
23
+ ]
19
24
 
20
25
  export function SidebarRail({
21
26
  onSwitchUser,
@@ -257,6 +262,11 @@ export function SidebarRail({
257
262
  <path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" /><path d="M14 2v7h7" />
258
263
  </svg>
259
264
  </NavItem>
265
+ <NavItem view="swarmfeed" label="Feed" expanded={railExpanded} isActive={isNavActive('swarmfeed')} onClick={() => handleNavClick('swarmfeed')}>
266
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
267
+ <path d="M4 11a9 9 0 0 1 9 9" /><path d="M4 4a16 16 0 0 1 16 16" /><circle cx="5" cy="19" r="1" />
268
+ </svg>
269
+ </NavItem>
260
270
  </div>
261
271
 
262
272
  <div className={`flex flex-col gap-0.5 ${railExpanded ? '' : 'items-center'}`}>
@@ -378,6 +388,48 @@ export function SidebarRail({
378
388
 
379
389
  {/* Bottom: Docs + Daemon + Settings + User */}
380
390
  <div className={`flex flex-col gap-1 ${railExpanded ? 'px-3' : 'items-center'}`}>
391
+ {railExpanded ? (
392
+ <div className="mb-1">
393
+ <div className="px-3 pb-1 text-[10px] font-700 uppercase tracking-[0.12em] text-text-3/45">Network</div>
394
+ <div className="flex flex-col gap-1">
395
+ {NETWORK_LINKS.map((link) => (
396
+ <a
397
+ key={link.href}
398
+ href={link.href}
399
+ target="_blank"
400
+ rel="noopener noreferrer"
401
+ className="w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all
402
+ bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04] no-underline"
403
+ style={{ fontFamily: 'inherit' }}
404
+ >
405
+ <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-[6px] border border-white/[0.08] bg-white/[0.03] text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/80">
406
+ {link.abbr}
407
+ </span>
408
+ {link.label}
409
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="ml-auto opacity-40">
410
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
411
+ </svg>
412
+ </a>
413
+ ))}
414
+ </div>
415
+ </div>
416
+ ) : (
417
+ <>
418
+ <div className="my-1 h-px w-6 bg-white/[0.06]" />
419
+ {NETWORK_LINKS.map((link) => (
420
+ <RailTooltip key={link.href} label={link.label} description="Open product site in a new tab">
421
+ <a
422
+ href={link.href}
423
+ target="_blank"
424
+ rel="noopener noreferrer"
425
+ className="rail-btn text-[10px] font-700 uppercase tracking-[0.08em] no-underline"
426
+ >
427
+ {link.abbr}
428
+ </a>
429
+ </RailTooltip>
430
+ ))}
431
+ </>
432
+ )}
381
433
  {railExpanded ? (
382
434
  <a
383
435
  href="https://swarmclaw.ai/docs"
@@ -0,0 +1,43 @@
1
+ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
2
+ import { useProtocolBuilderStore } from '@/features/protocols/builder/protocol-builder-store'
3
+
4
+ interface EdgeEditorProps {
5
+ edgeId: string | null
6
+ isOpen: boolean
7
+ onClose: () => void
8
+ }
9
+
10
+ export function EdgeEditor({ edgeId, isOpen, onClose }: EdgeEditorProps) {
11
+ const edges = useProtocolBuilderStore((s) => s.edges)
12
+ const updateEdgeData = useProtocolBuilderStore((s) => s.updateEdgeData)
13
+
14
+ if (!edgeId) return null
15
+ const edge = edges.find((e) => e.id === edgeId)
16
+ if (!edge) return null
17
+
18
+ return (
19
+ <Dialog open={isOpen} onOpenChange={onClose}>
20
+ <DialogContent>
21
+ <DialogTitle>Edit Edge</DialogTitle>
22
+ <div className="space-y-4">
23
+ <div>
24
+ <label className="text-sm font-semibold">Label</label>
25
+ <input
26
+ type="text"
27
+ value={edge.data?.label || ''}
28
+ onChange={(e) => updateEdgeData(edgeId, { label: e.target.value || null })}
29
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
30
+ placeholder="e.g., 'Yes', 'If unanimous'"
31
+ />
32
+ </div>
33
+ <div>
34
+ <label className="text-sm font-semibold">Type</label>
35
+ <div className="mt-1 rounded-md bg-muted px-2 py-1 text-sm capitalize">
36
+ {edge.data?.edgeType || 'default'}
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </DialogContent>
41
+ </Dialog>
42
+ )
43
+ }
@@ -0,0 +1,33 @@
1
+ import { BaseEdge, getBezierPath, type EdgeProps, type Edge } from '@xyflow/react'
2
+ import type { BuilderEdgeData } from '@/features/protocols/builder/protocol-builder-store'
3
+
4
+ export function BranchEdge(props: EdgeProps<Edge<BuilderEdgeData>>) {
5
+ const { sourceX, sourceY, targetX, targetY, markerEnd, selected, data } = props
6
+ const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, targetX, targetY })
7
+
8
+ return (
9
+ <>
10
+ <BaseEdge
11
+ path={edgePath}
12
+ markerEnd={markerEnd}
13
+ style={{
14
+ stroke: selected ? '#f59e0b' : '#d97706',
15
+ strokeWidth: selected ? 3 : 2,
16
+ }}
17
+ />
18
+ {data?.label && (
19
+ <foreignObject
20
+ x={labelX - 30}
21
+ y={labelY - 10}
22
+ width={60}
23
+ height={20}
24
+ className="pointer-events-none"
25
+ >
26
+ <div className="rounded border border-amber-500/30 bg-background px-1 py-0.5 text-center text-[10px] font-semibold text-amber-600">
27
+ {data.label}
28
+ </div>
29
+ </foreignObject>
30
+ )}
31
+ </>
32
+ )
33
+ }
@@ -0,0 +1,18 @@
1
+ import { BaseEdge, getBezierPath, type EdgeProps, type Edge } from '@xyflow/react'
2
+ import type { BuilderEdgeData } from '@/features/protocols/builder/protocol-builder-store'
3
+
4
+ export function DefaultEdge(props: EdgeProps<Edge<BuilderEdgeData>>) {
5
+ const { sourceX, sourceY, targetX, targetY, markerEnd, selected } = props
6
+ const [edgePath] = getBezierPath({ sourceX, sourceY, targetX, targetY })
7
+
8
+ return (
9
+ <BaseEdge
10
+ path={edgePath}
11
+ markerEnd={markerEnd}
12
+ style={{
13
+ stroke: selected ? '#3b82f6' : '#64748b',
14
+ strokeWidth: selected ? 3 : 2,
15
+ }}
16
+ />
17
+ )
18
+ }
@@ -0,0 +1,3 @@
1
+ export { DefaultEdge } from './default-edge'
2
+ export { BranchEdge } from './branch-edge'
3
+ export { LoopEdge } from './loop-edge'
@@ -0,0 +1,19 @@
1
+ import { BaseEdge, getBezierPath, type EdgeProps, type Edge } from '@xyflow/react'
2
+ import type { BuilderEdgeData } from '@/features/protocols/builder/protocol-builder-store'
3
+
4
+ export function LoopEdge(props: EdgeProps<Edge<BuilderEdgeData>>) {
5
+ const { sourceX, sourceY, targetX, targetY, markerEnd, selected } = props
6
+ const [edgePath] = getBezierPath({ sourceX, sourceY, targetX, targetY })
7
+
8
+ return (
9
+ <BaseEdge
10
+ path={edgePath}
11
+ markerEnd={markerEnd}
12
+ style={{
13
+ stroke: selected ? '#06b6d4' : '#0d9488',
14
+ strokeWidth: selected ? 3 : 2,
15
+ strokeDasharray: '5,5',
16
+ }}
17
+ />
18
+ )
19
+ }
@@ -0,0 +1,227 @@
1
+ import { useProtocolBuilderStore } from '@/features/protocols/builder/protocol-builder-store'
2
+ import { HintTip } from '@/components/shared/hint-tip'
3
+
4
+ const PHASE_KINDS = new Set([
5
+ 'present', 'collect_independent_inputs', 'round_robin',
6
+ 'compare', 'decide', 'summarize', 'emit_tasks',
7
+ 'dispatch_task', 'dispatch_delegation', 'wait',
8
+ ])
9
+
10
+ export function NodeInspector() {
11
+ const selectedNodeId = useProtocolBuilderStore((s) => s.selectedNodeId)
12
+ const nodes = useProtocolBuilderStore((s) => s.nodes)
13
+ const updateNodeData = useProtocolBuilderStore((s) => s.updateNodeData)
14
+ const pushUndo = useProtocolBuilderStore((s) => s.pushUndo)
15
+
16
+ const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) : null
17
+
18
+ if (!node) {
19
+ return (
20
+ <div className="rounded-lg border bg-card p-4 text-sm text-muted-foreground">
21
+ Select a node to edit properties
22
+ </div>
23
+ )
24
+ }
25
+
26
+ const { kind } = node.data
27
+
28
+ const update = (data: Parameters<typeof updateNodeData>[1]) => {
29
+ pushUndo()
30
+ updateNodeData(node.id, data)
31
+ }
32
+
33
+ return (
34
+ <div className="max-h-[480px] overflow-y-auto rounded-lg border bg-card p-4 shadow-sm">
35
+ <h3 className="mb-3 text-sm font-bold">Node Properties</h3>
36
+
37
+ {/* Label */}
38
+ <div className="mb-3">
39
+ <label className="text-xs font-semibold text-muted-foreground">Label</label>
40
+ <input
41
+ type="text"
42
+ value={node.data.label || ''}
43
+ onChange={(e) => update({ label: e.target.value })}
44
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
45
+ />
46
+ </div>
47
+
48
+ {/* Kind (read-only) */}
49
+ <div className="mb-3">
50
+ <label className="text-xs font-semibold text-muted-foreground">Kind</label>
51
+ <div className="mt-1 rounded-md bg-muted px-2 py-1 text-sm capitalize">
52
+ {kind.replace(/_/g, ' ')}
53
+ </div>
54
+ </div>
55
+
56
+ {/* Instructions (phases + actions) */}
57
+ {PHASE_KINDS.has(kind) && (
58
+ <div className="mb-3">
59
+ <div className="mb-1 flex items-center gap-1">
60
+ <label className="text-xs font-semibold text-muted-foreground">Instructions</label>
61
+ <HintTip text="Guidance for participants or the system during this step" />
62
+ </div>
63
+ <textarea
64
+ value={node.data.instructions || ''}
65
+ onChange={(e) => update({ instructions: e.target.value || null })}
66
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
67
+ rows={3}
68
+ placeholder="e.g., 'Present your analysis...'"
69
+ />
70
+ </div>
71
+ )}
72
+
73
+ {/* Turn Limit */}
74
+ {PHASE_KINDS.has(kind) && kind !== 'wait' && (
75
+ <div className="mb-3">
76
+ <label className="text-xs font-semibold text-muted-foreground">Max Turns</label>
77
+ <input
78
+ type="number"
79
+ value={node.data.turnLimit ?? ''}
80
+ onChange={(e) => update({ turnLimit: e.target.value ? parseInt(e.target.value) : null })}
81
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
82
+ placeholder="Optional"
83
+ min={1}
84
+ />
85
+ </div>
86
+ )}
87
+
88
+ {/* Completion Criteria */}
89
+ {['present', 'collect_independent_inputs', 'decide', 'summarize'].includes(kind) && (
90
+ <div className="mb-3">
91
+ <label className="text-xs font-semibold text-muted-foreground">Completion Criteria</label>
92
+ <textarea
93
+ value={node.data.completionCriteria || ''}
94
+ onChange={(e) => update({ completionCriteria: e.target.value || null })}
95
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
96
+ rows={2}
97
+ placeholder="e.g., 'All agents have provided input'"
98
+ />
99
+ </div>
100
+ )}
101
+
102
+ {/* Task Config (dispatch_task) */}
103
+ {kind === 'dispatch_task' && (
104
+ <>
105
+ <div className="mb-3">
106
+ <label className="text-xs font-semibold text-muted-foreground">Task Title</label>
107
+ <input
108
+ type="text"
109
+ value={node.data.taskConfig?.title || ''}
110
+ onChange={(e) =>
111
+ update({
112
+ taskConfig: {
113
+ title: e.target.value,
114
+ description: node.data.taskConfig?.description || '',
115
+ agentId: node.data.taskConfig?.agentId,
116
+ },
117
+ })
118
+ }
119
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
120
+ />
121
+ </div>
122
+ <div className="mb-3">
123
+ <label className="text-xs font-semibold text-muted-foreground">Task Description</label>
124
+ <textarea
125
+ value={node.data.taskConfig?.description || ''}
126
+ onChange={(e) =>
127
+ update({
128
+ taskConfig: {
129
+ title: node.data.taskConfig?.title || '',
130
+ description: e.target.value,
131
+ agentId: node.data.taskConfig?.agentId,
132
+ },
133
+ })
134
+ }
135
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
136
+ rows={2}
137
+ />
138
+ </div>
139
+ </>
140
+ )}
141
+
142
+ {/* Delegation Config (dispatch_delegation) */}
143
+ {kind === 'dispatch_delegation' && (
144
+ <>
145
+ <div className="mb-3">
146
+ <label className="text-xs font-semibold text-muted-foreground">Delegate to Agent</label>
147
+ <input
148
+ type="text"
149
+ value={node.data.delegationConfig?.agentId || ''}
150
+ onChange={(e) =>
151
+ update({
152
+ delegationConfig: {
153
+ agentId: e.target.value,
154
+ message: node.data.delegationConfig?.message || '',
155
+ },
156
+ })
157
+ }
158
+ placeholder="Agent ID"
159
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
160
+ />
161
+ </div>
162
+ <div className="mb-3">
163
+ <label className="text-xs font-semibold text-muted-foreground">Message</label>
164
+ <textarea
165
+ value={node.data.delegationConfig?.message || ''}
166
+ onChange={(e) =>
167
+ update({
168
+ delegationConfig: {
169
+ agentId: node.data.delegationConfig?.agentId || '',
170
+ message: e.target.value,
171
+ },
172
+ })
173
+ }
174
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
175
+ rows={2}
176
+ />
177
+ </div>
178
+ </>
179
+ )}
180
+
181
+ {/* Repeat Config */}
182
+ {kind === 'repeat' && (
183
+ <div className="mb-3">
184
+ <label className="text-xs font-semibold text-muted-foreground">Max Iterations</label>
185
+ <input
186
+ type="number"
187
+ value={node.data.repeat?.maxIterations ?? ''}
188
+ onChange={(e) =>
189
+ update({
190
+ repeat: {
191
+ bodyStepId: node.data.repeat?.bodyStepId || '',
192
+ maxIterations: parseInt(e.target.value) || 1,
193
+ exitCondition: node.data.repeat?.exitCondition,
194
+ onExhausted: node.data.repeat?.onExhausted,
195
+ },
196
+ })
197
+ }
198
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
199
+ min={1}
200
+ />
201
+ </div>
202
+ )}
203
+
204
+ {/* Branch Cases (summary) */}
205
+ {kind === 'branch' && (
206
+ <div className="mb-3">
207
+ <label className="text-xs font-semibold text-muted-foreground">Branch Cases</label>
208
+ <div className="mt-1 text-xs text-muted-foreground">
209
+ {node.data.branchCases?.length || 0} case(s) defined
210
+ </div>
211
+ </div>
212
+ )}
213
+
214
+ {/* Output Key */}
215
+ <div className="mb-3">
216
+ <label className="text-xs font-semibold text-muted-foreground">Output Key</label>
217
+ <input
218
+ type="text"
219
+ value={node.data.outputKey || ''}
220
+ onChange={(e) => update({ outputKey: e.target.value || null })}
221
+ className="mt-1 w-full rounded-md border bg-background px-2 py-1 text-sm"
222
+ placeholder="Optional key for step output"
223
+ />
224
+ </div>
225
+ </div>
226
+ )
227
+ }
@@ -0,0 +1,97 @@
1
+ import { useState, type DragEvent } from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { ProtocolStepKind } from '@/types'
4
+
5
+ interface PaletteCategory {
6
+ label: string
7
+ items: Array<{ kind: ProtocolStepKind; label: string; description: string }>
8
+ }
9
+
10
+ const CATEGORIES: PaletteCategory[] = [
11
+ {
12
+ label: 'Phases',
13
+ items: [
14
+ { kind: 'present', label: 'Present', description: 'Show info to participants' },
15
+ { kind: 'collect_independent_inputs', label: 'Collect Inputs', description: 'Gather independent responses' },
16
+ { kind: 'round_robin', label: 'Round Robin', description: 'Turn-based discussion' },
17
+ { kind: 'compare', label: 'Compare', description: 'Compare agent outputs' },
18
+ { kind: 'decide', label: 'Decide', description: 'Make a decision' },
19
+ { kind: 'summarize', label: 'Summarize', description: 'Synthesize results' },
20
+ ],
21
+ },
22
+ {
23
+ label: 'Actions',
24
+ items: [
25
+ { kind: 'emit_tasks', label: 'Emit Tasks', description: 'Create tasks from context' },
26
+ { kind: 'dispatch_task', label: 'Dispatch Task', description: 'Assign a specific task' },
27
+ { kind: 'dispatch_delegation', label: 'Delegate', description: 'Delegate to an agent' },
28
+ ],
29
+ },
30
+ {
31
+ label: 'Control Flow',
32
+ items: [
33
+ { kind: 'branch', label: 'Branch', description: 'Conditional path' },
34
+ { kind: 'repeat', label: 'Repeat', description: 'Loop with exit condition' },
35
+ { kind: 'parallel', label: 'Parallel', description: 'Fork into parallel branches' },
36
+ { kind: 'join', label: 'Join', description: 'Merge parallel branches' },
37
+ { kind: 'for_each', label: 'For Each', description: 'Iterate over items' },
38
+ ],
39
+ },
40
+ {
41
+ label: 'Advanced',
42
+ items: [
43
+ { kind: 'subflow', label: 'Subflow', description: 'Nested protocol template' },
44
+ { kind: 'swarm_claim', label: 'Swarm Claim', description: 'Competitive task claiming' },
45
+ { kind: 'wait', label: 'Wait', description: 'Pause until external input' },
46
+ { kind: 'complete', label: 'Complete', description: 'End the protocol' },
47
+ ],
48
+ },
49
+ ]
50
+
51
+ export function NodePalette() {
52
+ const [expandedCategory, setExpandedCategory] = useState<string | null>('Phases')
53
+
54
+ const onDragStart = (e: DragEvent, kind: ProtocolStepKind, label: string) => {
55
+ e.dataTransfer.effectAllowed = 'move'
56
+ e.dataTransfer.setData('application/x-protocol-node-kind', kind)
57
+ e.dataTransfer.setData('application/x-protocol-node-label', label)
58
+ }
59
+
60
+ return (
61
+ <div className="flex w-52 flex-col overflow-y-auto rounded-lg border bg-card p-3 shadow-sm">
62
+ <h3 className="mb-3 text-xs font-bold uppercase tracking-wider text-muted-foreground">
63
+ Drag to canvas
64
+ </h3>
65
+
66
+ {CATEGORIES.map((cat) => (
67
+ <div key={cat.label} className="mb-2">
68
+ <button
69
+ onClick={() => setExpandedCategory(expandedCategory === cat.label ? null : cat.label)}
70
+ className="mb-1 flex w-full items-center gap-1 text-xs font-semibold text-muted-foreground hover:text-foreground"
71
+ >
72
+ <span className="text-[10px]">{expandedCategory === cat.label ? '\u25BC' : '\u25B6'}</span>
73
+ {cat.label}
74
+ </button>
75
+ {expandedCategory === cat.label && (
76
+ <div className="space-y-1">
77
+ {cat.items.map(({ kind, label, description }) => (
78
+ <div
79
+ key={kind}
80
+ draggable
81
+ onDragStart={(e) => onDragStart(e, kind, label)}
82
+ className={cn(
83
+ 'cursor-grab rounded-md border bg-background px-3 py-2 text-sm',
84
+ 'transition-shadow hover:shadow-md active:cursor-grabbing',
85
+ )}
86
+ title={description}
87
+ >
88
+ {label}
89
+ </div>
90
+ ))}
91
+ </div>
92
+ )}
93
+ </div>
94
+ ))}
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,34 @@
1
+ import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
4
+
5
+ export function BranchNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ const cases = data.branchCases || []
7
+
8
+ return (
9
+ <div className={cn('relative', selected && 'ring-2 ring-blue-500 rounded')}>
10
+ <Handle type="target" position={Position.Top} />
11
+ <svg width="120" height="100" viewBox="0 0 120 100">
12
+ <polygon
13
+ points="60,5 115,50 60,95 5,50"
14
+ className="fill-amber-500/10 stroke-amber-500/40"
15
+ strokeWidth="2"
16
+ />
17
+ </svg>
18
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
19
+ <div className="text-xs font-semibold text-center px-4">{data.label}</div>
20
+ <div className="text-[10px] text-muted-foreground">{cases.length} case(s)</div>
21
+ </div>
22
+ {cases.map((bc, idx) => (
23
+ <Handle
24
+ key={bc.id}
25
+ type="source"
26
+ position={Position.Right}
27
+ id={bc.id}
28
+ style={{ top: `${25 + idx * (50 / Math.max(cases.length, 1))}%` }}
29
+ />
30
+ ))}
31
+ <Handle type="source" position={Position.Bottom} id="default" />
32
+ </div>
33
+ )
34
+ }
@@ -0,0 +1,17 @@
1
+ import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
4
+
5
+ export function CompleteNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ return (
7
+ <div
8
+ className={cn(
9
+ 'rounded-full border-2 border-emerald-500/40 bg-emerald-500/10 px-4 py-3 shadow-sm',
10
+ selected && 'ring-2 ring-blue-500',
11
+ )}
12
+ >
13
+ <Handle type="target" position={Position.Top} />
14
+ <div className="text-sm font-semibold text-center">{data.label || 'Complete'}</div>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,21 @@
1
+ import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
4
+
5
+ export function ForEachNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ return (
7
+ <div
8
+ className={cn(
9
+ 'rounded-lg border-2 border-sky-500/40 bg-sky-500/10 px-4 py-3 shadow-sm min-w-[140px]',
10
+ selected && 'ring-2 ring-blue-500',
11
+ )}
12
+ >
13
+ <Handle type="target" position={Position.Top} />
14
+ <div className="text-sm font-semibold">{data.label}</div>
15
+ <div className="mt-1 text-xs text-muted-foreground">
16
+ For each: {data.forEach?.itemAlias || 'item'}
17
+ </div>
18
+ <Handle type="source" position={Position.Bottom} />
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,9 @@
1
+ export { PhaseNode } from './phase-node'
2
+ export { BranchNode } from './branch-node'
3
+ export { LoopNode } from './loop-node'
4
+ export { ParallelNode } from './parallel-node'
5
+ export { JoinNode } from './join-node'
6
+ export { ForEachNode } from './for-each-node'
7
+ export { SubflowNode } from './subflow-node'
8
+ export { SwarmNode } from './swarm-node'
9
+ export { CompleteNode } from './complete-node'
@@ -0,0 +1,18 @@
1
+ import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
4
+
5
+ export function JoinNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ return (
7
+ <div
8
+ className={cn(
9
+ 'rounded-full border-2 border-pink-500/40 bg-pink-500/10 px-4 py-2 shadow-sm',
10
+ selected && 'ring-2 ring-blue-500',
11
+ )}
12
+ >
13
+ <Handle type="target" position={Position.Top} />
14
+ <div className="text-xs font-semibold text-center">{data.label || 'Join'}</div>
15
+ <Handle type="source" position={Position.Bottom} />
16
+ </div>
17
+ )
18
+ }