@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.
- package/README.md +32 -1
- package/package.json +9 -3
- package/src/.env.local +4 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +19 -0
- package/src/cli/spec.js +8 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +111 -107
- package/src/lib/providers/openai.ts +146 -142
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +100 -0
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- 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)
|
|
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,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
|
+
}
|