@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
@@ -0,0 +1,22 @@
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 LoopNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ return (
7
+ <div
8
+ className={cn(
9
+ 'rounded-lg border-2 border-teal-500/40 bg-teal-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
+ Max: {data.repeat?.maxIterations || '?'} iterations
17
+ </div>
18
+ <Handle type="source" position={Position.Bottom} />
19
+ <Handle type="source" position={Position.Left} id="loop-back" />
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,31 @@
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 ParallelNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ const branchCount = data.parallel?.branches.length || 1
7
+
8
+ return (
9
+ <div
10
+ className={cn(
11
+ 'rounded-lg border-2 border-pink-500/40 bg-pink-500/10 px-4 py-3 shadow-sm min-w-[140px]',
12
+ selected && 'ring-2 ring-blue-500',
13
+ )}
14
+ >
15
+ <Handle type="target" position={Position.Top} />
16
+ <div className="text-sm font-semibold">{data.label}</div>
17
+ <div className="mt-1 text-xs text-muted-foreground">
18
+ {branchCount} parallel {branchCount === 1 ? 'branch' : 'branches'}
19
+ </div>
20
+ {Array.from({ length: branchCount }).map((_, i) => (
21
+ <Handle
22
+ key={i}
23
+ type="source"
24
+ position={Position.Bottom}
25
+ id={`branch-${i}`}
26
+ style={{ left: `${((i + 1) / (branchCount + 1)) * 100}%` }}
27
+ />
28
+ ))}
29
+ </div>
30
+ )
31
+ }
@@ -0,0 +1,52 @@
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
+ const KIND_STYLES: Record<string, string> = {
6
+ present: 'border-blue-500/40 bg-blue-500/10',
7
+ collect_independent_inputs: 'border-cyan-500/40 bg-cyan-500/10',
8
+ round_robin: 'border-indigo-500/40 bg-indigo-500/10',
9
+ compare: 'border-yellow-500/40 bg-yellow-500/10',
10
+ decide: 'border-orange-500/40 bg-orange-500/10',
11
+ summarize: 'border-purple-500/40 bg-purple-500/10',
12
+ emit_tasks: 'border-green-500/40 bg-green-500/10',
13
+ wait: 'border-zinc-500/40 bg-zinc-500/10',
14
+ dispatch_task: 'border-lime-500/40 bg-lime-500/10',
15
+ dispatch_delegation: 'border-rose-500/40 bg-rose-500/10',
16
+ }
17
+
18
+ const RUNTIME_RING: Record<string, string> = {
19
+ completed: 'ring-2 ring-emerald-500 opacity-60',
20
+ running: 'ring-2 ring-blue-500 animate-pulse',
21
+ failed: 'ring-2 ring-red-500',
22
+ pending: 'opacity-40',
23
+ ready: 'ring-2 ring-amber-400',
24
+ }
25
+
26
+ export function PhaseNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
27
+ const style = KIND_STYLES[data.kind] || KIND_STYLES.present
28
+ const runtimeRing = data.runtimeStatus ? RUNTIME_RING[data.runtimeStatus] : ''
29
+
30
+ return (
31
+ <div
32
+ className={cn(
33
+ 'rounded-lg border-2 px-4 py-3 shadow-sm min-w-[140px]',
34
+ style,
35
+ runtimeRing,
36
+ selected && 'ring-2 ring-blue-500',
37
+ )}
38
+ >
39
+ <Handle type="target" position={Position.Top} />
40
+ <div className="text-sm font-semibold">{data.label}</div>
41
+ <div className="mt-1 text-xs text-muted-foreground capitalize">
42
+ {data.kind.replace(/_/g, ' ')}
43
+ </div>
44
+ {data.turnLimit && (
45
+ <div className="mt-1 text-xs text-muted-foreground">
46
+ Turns: {data.turnLimit}
47
+ </div>
48
+ )}
49
+ <Handle type="source" position={Position.Bottom} />
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,23 @@
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 SubflowNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ return (
7
+ <div
8
+ className={cn(
9
+ 'rounded-lg border-2 border-violet-500/40 bg-violet-500/10 px-4 py-3 shadow-md 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
+ {data.subflow?.templateId && (
16
+ <div className="mt-1 text-xs text-muted-foreground truncate max-w-[130px]">
17
+ Template: {data.subflow.templateId}
18
+ </div>
19
+ )}
20
+ <Handle type="source" position={Position.Bottom} />
21
+ </div>
22
+ )
23
+ }
@@ -0,0 +1,26 @@
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 SwarmNode({ data, selected }: NodeProps<Node<BuilderNodeData>>) {
6
+ return (
7
+ <div
8
+ className={cn(
9
+ 'rounded-lg border-2 border-green-500/40 bg-green-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
+ {data.swarm?.eligibleAgentIds.length || 0} agents
17
+ </div>
18
+ {data.swarm?.claimLimitPerAgent && (
19
+ <div className="text-xs text-muted-foreground">
20
+ Limit: {data.swarm.claimLimitPerAgent}/agent
21
+ </div>
22
+ )}
23
+ <Handle type="source" position={Position.Bottom} />
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,184 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useMemo, type DragEvent } from 'react'
4
+ import {
5
+ ReactFlow,
6
+ Background,
7
+ Controls,
8
+ MiniMap,
9
+ applyNodeChanges,
10
+ applyEdgeChanges,
11
+ type Connection,
12
+ type NodeChange,
13
+ type EdgeChange,
14
+ type Node,
15
+ } from '@xyflow/react'
16
+ import '@xyflow/react/dist/style.css'
17
+ import { useProtocolBuilderStore, type BuilderNodeData } from '@/features/protocols/builder/protocol-builder-store'
18
+ import { getNodeTypeForKind } from '@/features/protocols/builder/utils/template-to-nodes'
19
+ import { PhaseNode, BranchNode, LoopNode, ParallelNode, JoinNode, ForEachNode, SubflowNode, SwarmNode, CompleteNode } from './node-types'
20
+ import { DefaultEdge, BranchEdge, LoopEdge } from './edge-types'
21
+ import { NodePalette } from './node-palette'
22
+ import { NodeInspector } from './node-inspector'
23
+ import { ValidationPanel } from './validation-panel'
24
+ import type { ProtocolStepKind } from '@/types'
25
+
26
+ const nodeTypes = {
27
+ phase: PhaseNode,
28
+ branch: BranchNode,
29
+ loop: LoopNode,
30
+ parallel: ParallelNode,
31
+ join: JoinNode,
32
+ forEach: ForEachNode,
33
+ subflow: SubflowNode,
34
+ swarm: SwarmNode,
35
+ complete: CompleteNode,
36
+ }
37
+
38
+ const edgeTypes = {
39
+ default: DefaultEdge,
40
+ branch: BranchEdge,
41
+ loop: LoopEdge,
42
+ }
43
+
44
+ export function ProtocolBuilderCanvas() {
45
+ const nodes = useProtocolBuilderStore((s) => s.nodes)
46
+ const edges = useProtocolBuilderStore((s) => s.edges)
47
+ const setNodes = useProtocolBuilderStore((s) => s.setNodes)
48
+ const setEdges = useProtocolBuilderStore((s) => s.setEdges)
49
+ const selectNode = useProtocolBuilderStore((s) => s.selectNode)
50
+ const selectEdge = useProtocolBuilderStore((s) => s.selectEdge)
51
+ const addNode = useProtocolBuilderStore((s) => s.addNode)
52
+ const addEdge = useProtocolBuilderStore((s) => s.addEdge)
53
+ const pushUndo = useProtocolBuilderStore((s) => s.pushUndo)
54
+ const isDirty = useProtocolBuilderStore((s) => s.isDirty)
55
+ const undo = useProtocolBuilderStore((s) => s.undo)
56
+ const redo = useProtocolBuilderStore((s) => s.redo)
57
+
58
+ const onNodesChange = useCallback(
59
+ (changes: NodeChange<Node<BuilderNodeData>>[]) => {
60
+ setNodes(applyNodeChanges(changes, nodes))
61
+ },
62
+ [nodes, setNodes],
63
+ )
64
+
65
+ const onEdgesChange = useCallback(
66
+ (changes: EdgeChange[]) => {
67
+ setEdges(applyEdgeChanges(changes, edges) as typeof edges)
68
+ },
69
+ [edges, setEdges],
70
+ )
71
+
72
+ const onConnect = useCallback(
73
+ (connection: Connection) => {
74
+ pushUndo()
75
+ addEdge({
76
+ id: `${connection.source}--${connection.target}--${Date.now()}`,
77
+ source: connection.source!,
78
+ target: connection.target!,
79
+ sourceHandle: connection.sourceHandle,
80
+ targetHandle: connection.targetHandle,
81
+ type: 'default',
82
+ data: { edgeType: 'default' },
83
+ })
84
+ },
85
+ [addEdge, pushUndo],
86
+ )
87
+
88
+ const onNodeClick = useCallback(
89
+ (_event: React.MouseEvent, node: Node) => {
90
+ selectNode(node.id)
91
+ },
92
+ [selectNode],
93
+ )
94
+
95
+ const onEdgeClick = useCallback(
96
+ (_event: React.MouseEvent, edge: { id: string }) => {
97
+ selectEdge(edge.id)
98
+ },
99
+ [selectEdge],
100
+ )
101
+
102
+ const onPaneClick = useCallback(() => {
103
+ selectNode(null)
104
+ selectEdge(null)
105
+ }, [selectNode, selectEdge])
106
+
107
+ const onDragOver = useCallback((e: DragEvent) => {
108
+ e.preventDefault()
109
+ e.dataTransfer.dropEffect = 'move'
110
+ }, [])
111
+
112
+ const onDrop = useCallback(
113
+ (e: DragEvent) => {
114
+ e.preventDefault()
115
+ const kind = e.dataTransfer.getData('application/x-protocol-node-kind') as ProtocolStepKind
116
+ const label = e.dataTransfer.getData('application/x-protocol-node-label')
117
+ if (!kind) return
118
+
119
+ pushUndo()
120
+
121
+ const nodeData: BuilderNodeData = { label: label || kind, kind }
122
+ const newNode: Node<BuilderNodeData> = {
123
+ id: crypto.randomUUID(),
124
+ type: getNodeTypeForKind(kind),
125
+ position: { x: e.nativeEvent.offsetX - 70, y: e.nativeEvent.offsetY - 30 },
126
+ data: nodeData,
127
+ }
128
+ addNode(newNode)
129
+ },
130
+ [addNode, pushUndo],
131
+ )
132
+
133
+ const onKeyDown = useCallback(
134
+ (e: React.KeyboardEvent) => {
135
+ if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
136
+ e.preventDefault()
137
+ if (e.shiftKey) redo()
138
+ else undo()
139
+ }
140
+ },
141
+ [undo, redo],
142
+ )
143
+
144
+ const memoizedNodeTypes = useMemo(() => nodeTypes, [])
145
+ const memoizedEdgeTypes = useMemo(() => edgeTypes, [])
146
+
147
+ return (
148
+ <div className="flex h-full w-full gap-3" onKeyDown={onKeyDown} tabIndex={0}>
149
+ <NodePalette />
150
+ <div className="relative flex-1 overflow-hidden rounded-lg border">
151
+ {isDirty && (
152
+ <div className="absolute left-1/2 top-2 z-10 -translate-x-1/2 rounded bg-amber-500/10 px-2 py-1 text-xs text-amber-500">
153
+ Unsaved changes
154
+ </div>
155
+ )}
156
+ <ReactFlow
157
+ nodes={nodes}
158
+ edges={edges}
159
+ nodeTypes={memoizedNodeTypes}
160
+ edgeTypes={memoizedEdgeTypes}
161
+ onNodesChange={onNodesChange}
162
+ onEdgesChange={onEdgesChange}
163
+ onConnect={onConnect}
164
+ onNodeClick={onNodeClick}
165
+ onEdgeClick={onEdgeClick}
166
+ onPaneClick={onPaneClick}
167
+ onDragOver={onDragOver}
168
+ onDrop={onDrop}
169
+ fitView
170
+ deleteKeyCode="Delete"
171
+ defaultEdgeOptions={{ type: 'default', data: { edgeType: 'default' } }}
172
+ >
173
+ <Background />
174
+ <Controls />
175
+ <MiniMap />
176
+ </ReactFlow>
177
+ </div>
178
+ <div className="flex w-72 flex-col gap-3">
179
+ <NodeInspector />
180
+ <ValidationPanel />
181
+ </div>
182
+ </div>
183
+ )
184
+ }
@@ -0,0 +1,29 @@
1
+ import { useProtocolBuilderStore } from '@/features/protocols/builder/protocol-builder-store'
2
+ import { useProtocolRunDetailQuery } from '@/features/protocols/queries'
3
+
4
+ export function RunOverlay() {
5
+ const activeRunId = useProtocolBuilderStore((s) => s.activeRunId)
6
+ const nodes = useProtocolBuilderStore((s) => s.nodes)
7
+ const { data: runDetail } = useProtocolRunDetailQuery(activeRunId)
8
+
9
+ if (!runDetail) return null
10
+
11
+ const { run } = runDetail
12
+ const currentNode = run.currentStepId
13
+ ? nodes.find((n) => n.id === run.currentStepId)
14
+ : null
15
+
16
+ return (
17
+ <div className="absolute left-4 top-4 z-50 rounded-lg border border-blue-500/30 bg-card p-3 shadow-lg">
18
+ <div className="text-sm font-semibold">{run.title}</div>
19
+ <div className="mt-1 text-xs text-muted-foreground">
20
+ Status: <span className="font-semibold capitalize">{run.status}</span>
21
+ </div>
22
+ {currentNode && (
23
+ <div className="mt-1 text-xs text-muted-foreground">
24
+ Current: {currentNode.data.label}
25
+ </div>
26
+ )}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,53 @@
1
+ import { useProtocolTemplatesQuery } from '@/features/protocols/queries'
2
+ import { useRouter } from 'next/navigation'
3
+ import { cn } from '@/lib/utils'
4
+ import type { ProtocolTemplate } from '@/types'
5
+
6
+ export function TemplateGallery() {
7
+ const { data: templates } = useProtocolTemplatesQuery()
8
+ const router = useRouter()
9
+
10
+ const builtInTemplates = templates?.filter((t) => t.builtIn) || []
11
+ const customTemplates = templates?.filter((t) => !t.builtIn) || []
12
+
13
+ const renderCard = (template: ProtocolTemplate) => (
14
+ <button
15
+ key={template.id}
16
+ onClick={() => router.push(`/protocols/builder/${template.id}`)}
17
+ className={cn(
18
+ 'rounded-lg border bg-card p-4 text-left transition-shadow hover:shadow-md',
19
+ )}
20
+ >
21
+ <div className="text-sm font-semibold">{template.name}</div>
22
+ <div className="mt-1 text-xs text-muted-foreground line-clamp-2">
23
+ {template.description}
24
+ </div>
25
+ {template.tags && template.tags.length > 0 && (
26
+ <div className="mt-2 flex gap-1">
27
+ {template.tags.slice(0, 2).map((tag) => (
28
+ <span key={tag} className="rounded bg-muted px-1.5 py-0.5 text-[10px]">
29
+ {tag}
30
+ </span>
31
+ ))}
32
+ </div>
33
+ )}
34
+ </button>
35
+ )
36
+
37
+ return (
38
+ <div className="space-y-4">
39
+ {builtInTemplates.length > 0 && (
40
+ <div>
41
+ <h4 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Built-in</h4>
42
+ <div className="grid grid-cols-2 gap-3">{builtInTemplates.map(renderCard)}</div>
43
+ </div>
44
+ )}
45
+ {customTemplates.length > 0 && (
46
+ <div>
47
+ <h4 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Custom</h4>
48
+ <div className="grid grid-cols-2 gap-3">{customTemplates.map(renderCard)}</div>
49
+ </div>
50
+ )}
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,57 @@
1
+ import { useProtocolBuilderStore } from '@/features/protocols/builder/protocol-builder-store'
2
+
3
+ export function ValidationPanel() {
4
+ const errors = useProtocolBuilderStore((s) => s.validationErrors)
5
+ const warnings = useProtocolBuilderStore((s) => s.validationWarnings)
6
+ const selectNode = useProtocolBuilderStore((s) => s.selectNode)
7
+
8
+ if (errors.length === 0 && warnings.length === 0) {
9
+ return (
10
+ <div className="rounded-lg border bg-card p-3">
11
+ <div className="text-sm font-semibold text-emerald-500">All checks passed</div>
12
+ </div>
13
+ )
14
+ }
15
+
16
+ return (
17
+ <div className="max-h-48 overflow-y-auto rounded-lg border bg-card p-3 shadow-sm">
18
+ <h3 className="mb-2 text-sm font-bold">Validation</h3>
19
+
20
+ {errors.length > 0 && (
21
+ <div className="mb-2">
22
+ <h4 className="mb-1 text-xs font-semibold text-red-500">Errors</h4>
23
+ <ul className="space-y-1">
24
+ {errors.map((err, i) => (
25
+ <li key={i} className="text-xs text-red-400">
26
+ <button
27
+ onClick={() => err.nodeId && selectNode(err.nodeId)}
28
+ className="text-left hover:underline"
29
+ >
30
+ {err.message}
31
+ </button>
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </div>
36
+ )}
37
+
38
+ {warnings.length > 0 && (
39
+ <div>
40
+ <h4 className="mb-1 text-xs font-semibold text-yellow-500">Warnings</h4>
41
+ <ul className="space-y-1">
42
+ {warnings.map((warn, i) => (
43
+ <li key={i} className="text-xs text-yellow-400">
44
+ <button
45
+ onClick={() => warn.nodeId && selectNode(warn.nodeId)}
46
+ className="text-left hover:underline"
47
+ >
48
+ {warn.message}
49
+ </button>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </div>
54
+ )}
55
+ </div>
56
+ )
57
+ }
@@ -555,7 +555,7 @@ export function SkillsWorkspace() {
555
555
  Search the marketplace, use Details to learn what a tool does, or Open listing to visit the source page.
556
556
  </p>
557
557
 
558
- <div className="mt-4 grid gap-2 sm:grid-cols-[minmax(240px,1fr)_auto_auto_auto]">
558
+ <div className="mt-4 grid gap-2 sm:grid-cols-[minmax(240px,1fr)_auto_auto]">
559
559
  <SearchField
560
560
  value={hubQuery}
561
561
  onChange={setHubQuery}
@@ -577,14 +577,6 @@ export function SkillsWorkspace() {
577
577
  >
578
578
  Search now
579
579
  </button>
580
- <a
581
- href="https://clawhub.ai/skills"
582
- target="_blank"
583
- rel="noreferrer"
584
- className={ghostButtonClassName}
585
- >
586
- Open ClawHub
587
- </a>
588
580
  </div>
589
581
 
590
582
  <FilterRow
@@ -0,0 +1,2 @@
1
+ export { useTemplateSync } from './use-template-sync'
2
+ export { useCanvasValidation } from './use-canvas-validation'
@@ -0,0 +1,14 @@
1
+ import { useEffect } from 'react'
2
+ import { useProtocolBuilderStore } from '../protocol-builder-store'
3
+ import { validateDAG } from '../validators/dag-validator'
4
+
5
+ export function useCanvasValidation() {
6
+ const nodes = useProtocolBuilderStore((s) => s.nodes)
7
+ const edges = useProtocolBuilderStore((s) => s.edges)
8
+ const setValidation = useProtocolBuilderStore((s) => s.setValidation)
9
+
10
+ useEffect(() => {
11
+ const { errors, warnings } = validateDAG(nodes, edges)
12
+ setValidation(errors, warnings)
13
+ }, [nodes, edges, setValidation])
14
+ }
@@ -0,0 +1,39 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { useProtocolBuilderStore, type BuilderNode } from '../protocol-builder-store'
3
+ import { useProtocolRunDetailQuery } from '@/features/protocols/queries'
4
+
5
+ export function useRunOverlay(runId: string | null) {
6
+ const setActiveRun = useProtocolBuilderStore((s) => s.setActiveRun)
7
+ const nodes = useProtocolBuilderStore((s) => s.nodes)
8
+ const setNodes = useProtocolBuilderStore((s) => s.setNodes)
9
+ const { data: runDetail } = useProtocolRunDetailQuery(runId)
10
+
11
+ const prevStepStateRef = useRef<string | null>(null)
12
+
13
+ useEffect(() => {
14
+ setActiveRun(runId)
15
+ return () => setActiveRun(null)
16
+ }, [runId, setActiveRun])
17
+
18
+ useEffect(() => {
19
+ if (!runDetail?.run?.stepState) return
20
+
21
+ const stepStateKey = JSON.stringify(runDetail.run.stepState)
22
+ if (stepStateKey === prevStepStateRef.current) return
23
+ prevStepStateRef.current = stepStateKey
24
+
25
+ const updated: BuilderNode[] = nodes.map((node) => {
26
+ const stepState = runDetail.run.stepState?.[node.id]
27
+ if (!stepState) {
28
+ if (node.data.runtimeStatus) {
29
+ return { ...node, data: { ...node.data, runtimeStatus: null } }
30
+ }
31
+ return node
32
+ }
33
+ if (node.data.runtimeStatus === stepState.status) return node
34
+ return { ...node, data: { ...node.data, runtimeStatus: stepState.status } }
35
+ })
36
+
37
+ setNodes(updated)
38
+ }, [runDetail?.run?.stepState, nodes, setNodes])
39
+ }
@@ -0,0 +1,45 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { useProtocolBuilderStore } from '../protocol-builder-store'
3
+ import { useUpsertProtocolTemplateMutation, type ProtocolTemplatePayload } from '@/features/protocols/queries'
4
+ import { nodesToTemplate } from '../utils/nodes-to-template'
5
+
6
+ export function useTemplateSync(autoSaveDelayMs = 2000) {
7
+ const nodes = useProtocolBuilderStore((s) => s.nodes)
8
+ const edges = useProtocolBuilderStore((s) => s.edges)
9
+ const isDirty = useProtocolBuilderStore((s) => s.isDirty)
10
+ const currentTemplate = useProtocolBuilderStore((s) => s.currentTemplate)
11
+ const setDirty = useProtocolBuilderStore((s) => s.setDirty)
12
+ const validationErrors = useProtocolBuilderStore((s) => s.validationErrors)
13
+ const mutation = useUpsertProtocolTemplateMutation()
14
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
15
+
16
+ useEffect(() => {
17
+ if (!isDirty || !currentTemplate || validationErrors.length > 0) return
18
+
19
+ if (debounceRef.current) clearTimeout(debounceRef.current)
20
+
21
+ debounceRef.current = setTimeout(async () => {
22
+ const updated = nodesToTemplate(nodes, edges, currentTemplate)
23
+ const payload: ProtocolTemplatePayload = {
24
+ name: updated.name,
25
+ description: updated.description,
26
+ tags: updated.tags || [],
27
+ recommendedOutputs: updated.recommendedOutputs || [],
28
+ singleAgentAllowed: updated.singleAgentAllowed || false,
29
+ steps: updated.steps || [],
30
+ entryStepId: updated.entryStepId,
31
+ }
32
+
33
+ try {
34
+ await mutation.mutateAsync({ templateId: currentTemplate.id, payload })
35
+ setDirty(false)
36
+ } catch {
37
+ // Save failed silently — user sees "Unsaved changes" indicator
38
+ }
39
+ }, autoSaveDelayMs)
40
+
41
+ return () => {
42
+ if (debounceRef.current) clearTimeout(debounceRef.current)
43
+ }
44
+ }, [isDirty, nodes, edges, currentTemplate, validationErrors.length, autoSaveDelayMs, mutation, setDirty])
45
+ }