minimal-workflow 0.2.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/index.html +12 -0
- package/package.json +44 -0
- package/public/manifest.fallback.json +482 -0
- package/scripts/cli.ts +318 -0
- package/scripts/server.ts +131 -0
- package/src/App.tsx +119 -0
- package/src/api/files.ts +32 -0
- package/src/api/manifest.ts +17 -0
- package/src/canvas/WorkflowCanvas.tsx +161 -0
- package/src/canvas/edges/SequentialEdge.tsx +14 -0
- package/src/canvas/nodes/AssertNode.tsx +17 -0
- package/src/canvas/nodes/BaseNode.tsx +55 -0
- package/src/canvas/nodes/BranchNode.tsx +21 -0
- package/src/canvas/nodes/LlmNode.tsx +29 -0
- package/src/canvas/nodes/LoopNode.tsx +22 -0
- package/src/canvas/nodes/PauseNode.tsx +17 -0
- package/src/canvas/nodes/SkillNode.tsx +27 -0
- package/src/canvas/nodes/ToolNode.tsx +27 -0
- package/src/index.css +379 -0
- package/src/inspector/Inspector.tsx +224 -0
- package/src/inspector/LlmNodeForm.tsx +59 -0
- package/src/inspector/SkillNodeForm.tsx +62 -0
- package/src/inspector/ToolNodeForm.tsx +120 -0
- package/src/main.tsx +10 -0
- package/src/palette/ControlFlowSection.tsx +45 -0
- package/src/palette/Palette.tsx +65 -0
- package/src/palette/SkillPaletteSection.tsx +31 -0
- package/src/palette/ToolPaletteSection.tsx +31 -0
- package/src/store/filesStore.ts +29 -0
- package/src/store/manifestStore.ts +25 -0
- package/src/store/workflowStore.ts +140 -0
- package/src/types.ts +83 -0
- package/src/utils/safePath.ts +49 -0
- package/src/yaml-view/YamlEditor.tsx +71 -0
- package/src/yaml-view/sync.ts +246 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +21 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ReactFlow,
|
|
4
|
+
Background,
|
|
5
|
+
Controls,
|
|
6
|
+
MiniMap,
|
|
7
|
+
applyNodeChanges,
|
|
8
|
+
applyEdgeChanges,
|
|
9
|
+
type NodeChange,
|
|
10
|
+
type EdgeChange,
|
|
11
|
+
type Connection,
|
|
12
|
+
addEdge,
|
|
13
|
+
} from '@xyflow/react';
|
|
14
|
+
import '@xyflow/react/dist/style.css';
|
|
15
|
+
|
|
16
|
+
import { useWorkflowStore } from '../store/workflowStore.ts';
|
|
17
|
+
import { ToolNode } from './nodes/ToolNode.tsx';
|
|
18
|
+
import { LlmNode } from './nodes/LlmNode.tsx';
|
|
19
|
+
import { SkillNode } from './nodes/SkillNode.tsx';
|
|
20
|
+
import { AssertNode } from './nodes/AssertNode.tsx';
|
|
21
|
+
import { PauseNode } from './nodes/PauseNode.tsx';
|
|
22
|
+
import { BranchNode } from './nodes/BranchNode.tsx';
|
|
23
|
+
import { LoopNode } from './nodes/LoopNode.tsx';
|
|
24
|
+
import { SequentialEdge } from './edges/SequentialEdge.tsx';
|
|
25
|
+
import type { StepDef, StepKind } from '../types.ts';
|
|
26
|
+
|
|
27
|
+
const nodeTypes = {
|
|
28
|
+
tool: ToolNode,
|
|
29
|
+
llm: LlmNode,
|
|
30
|
+
skill: SkillNode,
|
|
31
|
+
assert: AssertNode,
|
|
32
|
+
pause: PauseNode,
|
|
33
|
+
branch: BranchNode,
|
|
34
|
+
loop: LoopNode,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const edgeTypes = {
|
|
38
|
+
sequential: SequentialEdge,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function WorkflowCanvas() {
|
|
42
|
+
const nodes = useWorkflowStore((s) => s.nodes);
|
|
43
|
+
const edges = useWorkflowStore((s) => s.edges);
|
|
44
|
+
const setNodes = useWorkflowStore((s) => s.setNodes);
|
|
45
|
+
const setEdges = useWorkflowStore((s) => s.setEdges);
|
|
46
|
+
const selectNode = useWorkflowStore((s) => s.selectNode);
|
|
47
|
+
const addStep = useWorkflowStore((s) => s.addStep);
|
|
48
|
+
|
|
49
|
+
const onNodesChange = useCallback(
|
|
50
|
+
(changes: NodeChange[]) => {
|
|
51
|
+
setNodes(applyNodeChanges(changes, nodes) as typeof nodes);
|
|
52
|
+
},
|
|
53
|
+
[nodes, setNodes],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const onEdgesChange = useCallback(
|
|
57
|
+
(changes: EdgeChange[]) => {
|
|
58
|
+
setEdges(applyEdgeChanges(changes, edges));
|
|
59
|
+
},
|
|
60
|
+
[edges, setEdges],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const onConnect = useCallback(
|
|
64
|
+
(conn: Connection) => {
|
|
65
|
+
setEdges(addEdge({ ...conn, type: 'sequential' }, edges));
|
|
66
|
+
},
|
|
67
|
+
[edges, setEdges],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const onNodeClick = useCallback(
|
|
71
|
+
(_: React.MouseEvent, node: { id: string }) => {
|
|
72
|
+
selectNode(node.id);
|
|
73
|
+
},
|
|
74
|
+
[selectNode],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const onPaneClick = useCallback(() => selectNode(null), [selectNode]);
|
|
78
|
+
|
|
79
|
+
const onDragOver = useCallback((e: React.DragEvent) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
e.dataTransfer.dropEffect = 'move';
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const onDrop = useCallback(
|
|
85
|
+
(e: React.DragEvent) => {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const raw = e.dataTransfer.getData('application/workflow-ui-node');
|
|
88
|
+
if (!raw) return;
|
|
89
|
+
let payload: { kind: StepKind; name?: string };
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(raw);
|
|
92
|
+
} catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const bounds = (e.target as HTMLElement)
|
|
96
|
+
.closest('.canvas')!
|
|
97
|
+
.getBoundingClientRect();
|
|
98
|
+
const position = {
|
|
99
|
+
x: e.clientX - bounds.left - 90,
|
|
100
|
+
y: e.clientY - bounds.top - 30,
|
|
101
|
+
};
|
|
102
|
+
const id = nextId(payload.kind, nodes.map((n) => n.id));
|
|
103
|
+
const step = makeStepTemplate(payload.kind, id, payload.name);
|
|
104
|
+
addStep(step, position);
|
|
105
|
+
},
|
|
106
|
+
[nodes, addStep],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const defaultViewport = useMemo(() => ({ x: 0, y: 0, zoom: 1 }), []);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="canvas" onDragOver={onDragOver} onDrop={onDrop}>
|
|
113
|
+
<ReactFlow
|
|
114
|
+
nodes={nodes}
|
|
115
|
+
edges={edges}
|
|
116
|
+
onNodesChange={onNodesChange}
|
|
117
|
+
onEdgesChange={onEdgesChange}
|
|
118
|
+
onConnect={onConnect}
|
|
119
|
+
onNodeClick={onNodeClick}
|
|
120
|
+
onPaneClick={onPaneClick}
|
|
121
|
+
nodeTypes={nodeTypes}
|
|
122
|
+
edgeTypes={edgeTypes}
|
|
123
|
+
defaultViewport={defaultViewport}
|
|
124
|
+
fitView={nodes.length > 0}
|
|
125
|
+
>
|
|
126
|
+
<Background />
|
|
127
|
+
<Controls />
|
|
128
|
+
<MiniMap pannable zoomable />
|
|
129
|
+
</ReactFlow>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function nextId(kind: StepKind, existing: string[]): string {
|
|
135
|
+
let i = 1;
|
|
136
|
+
while (existing.includes(`${kind}_${i}`)) i++;
|
|
137
|
+
return `${kind}_${i}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function makeStepTemplate(
|
|
141
|
+
kind: StepKind,
|
|
142
|
+
id: string,
|
|
143
|
+
name?: string,
|
|
144
|
+
): StepDef {
|
|
145
|
+
switch (kind) {
|
|
146
|
+
case 'tool':
|
|
147
|
+
return { id, tool: name ?? 'Read', args: {} };
|
|
148
|
+
case 'llm':
|
|
149
|
+
return { id, llm: 'TODO: 写 prompt' };
|
|
150
|
+
case 'skill':
|
|
151
|
+
return { id, skill: name ?? 'commit', input: '' };
|
|
152
|
+
case 'assert':
|
|
153
|
+
return { id, type: 'assert', condition: 'true', onFail: '' };
|
|
154
|
+
case 'pause':
|
|
155
|
+
return { id, type: 'pause', prompt: '' };
|
|
156
|
+
case 'branch':
|
|
157
|
+
return { id, type: 'branch', condition: 'false', then: [], else: [] };
|
|
158
|
+
case 'loop':
|
|
159
|
+
return { id, type: 'loop', over: '[]', as: 'item', do: [] };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseEdge, getBezierPath } from '@xyflow/react';
|
|
2
|
+
import type { EdgeProps } from '@xyflow/react';
|
|
3
|
+
|
|
4
|
+
export function SequentialEdge(props: EdgeProps) {
|
|
5
|
+
const [edgePath] = getBezierPath({
|
|
6
|
+
sourceX: props.sourceX,
|
|
7
|
+
sourceY: props.sourceY,
|
|
8
|
+
sourcePosition: props.sourcePosition,
|
|
9
|
+
targetX: props.targetX,
|
|
10
|
+
targetY: props.targetY,
|
|
11
|
+
targetPosition: props.targetPosition,
|
|
12
|
+
});
|
|
13
|
+
return <BaseEdge id={props.id} path={edgePath} markerEnd={props.markerEnd} />;
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function AssertNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
return (
|
|
9
|
+
<BaseNode
|
|
10
|
+
{...props}
|
|
11
|
+
data={data}
|
|
12
|
+
variant="assert"
|
|
13
|
+
title="⚠ assert"
|
|
14
|
+
body={step.condition ?? '(no condition)'}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Handle, Position } from '@xyflow/react';
|
|
2
|
+
import type { NodeProps } from '@xyflow/react';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
interface BaseNodeProps extends NodeProps {
|
|
6
|
+
data: CanvasNodeData;
|
|
7
|
+
selected?: boolean;
|
|
8
|
+
/** 节点种类 css class(决定边框色) */
|
|
9
|
+
variant: string;
|
|
10
|
+
/** 节点标题(如 "🔧 tool: bash") */
|
|
11
|
+
title: string;
|
|
12
|
+
/** 节点正文(摘要) */
|
|
13
|
+
body?: string;
|
|
14
|
+
/** 多个出口端口(branch/loop 用);省略则单 source */
|
|
15
|
+
outputs?: { id: string; label: string }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function BaseNode({
|
|
19
|
+
data,
|
|
20
|
+
selected,
|
|
21
|
+
variant,
|
|
22
|
+
title,
|
|
23
|
+
body,
|
|
24
|
+
outputs,
|
|
25
|
+
}: BaseNodeProps) {
|
|
26
|
+
const errors = data.errors;
|
|
27
|
+
return (
|
|
28
|
+
<div className={`canvas-node ${variant} ${selected ? 'selected' : ''}`}>
|
|
29
|
+
<Handle type="target" position={Position.Top} />
|
|
30
|
+
<div className="node-header">
|
|
31
|
+
<span>{title}</span>
|
|
32
|
+
<span className="node-id">{data.step.id}</span>
|
|
33
|
+
</div>
|
|
34
|
+
{body && <div className="node-body">{body}</div>}
|
|
35
|
+
{errors && errors.length > 0 && (
|
|
36
|
+
<div style={{ marginTop: 4 }}>
|
|
37
|
+
<span className="badge err">{errors.length} 错误</span>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
{!outputs || outputs.length === 0 ? (
|
|
41
|
+
<Handle type="source" position={Position.Bottom} />
|
|
42
|
+
) : (
|
|
43
|
+
outputs.map((o, i) => (
|
|
44
|
+
<Handle
|
|
45
|
+
key={o.id}
|
|
46
|
+
type="source"
|
|
47
|
+
position={Position.Bottom}
|
|
48
|
+
id={o.id}
|
|
49
|
+
style={{ left: `${((i + 1) * 100) / (outputs.length + 1)}%` }}
|
|
50
|
+
/>
|
|
51
|
+
))
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function BranchNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
return (
|
|
9
|
+
<BaseNode
|
|
10
|
+
{...props}
|
|
11
|
+
data={data}
|
|
12
|
+
variant="branch"
|
|
13
|
+
title="🔀 branch"
|
|
14
|
+
body={step.condition ?? '(no condition)'}
|
|
15
|
+
outputs={[
|
|
16
|
+
{ id: 'then', label: 'then' },
|
|
17
|
+
{ id: 'else', label: 'else' },
|
|
18
|
+
]}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function LlmNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
const prompt =
|
|
9
|
+
typeof step.llm === 'string'
|
|
10
|
+
? step.llm
|
|
11
|
+
: typeof step.llm === 'object' && step.llm !== null
|
|
12
|
+
? ((step.llm as Record<string, unknown>).prompt as string) ||
|
|
13
|
+
JSON.stringify(step.llm)
|
|
14
|
+
: '';
|
|
15
|
+
const preview = prompt
|
|
16
|
+
? prompt.length > 80
|
|
17
|
+
? `${prompt.slice(0, 80)}...`
|
|
18
|
+
: prompt
|
|
19
|
+
: '(no prompt)';
|
|
20
|
+
return (
|
|
21
|
+
<BaseNode
|
|
22
|
+
{...props}
|
|
23
|
+
data={data}
|
|
24
|
+
variant="llm"
|
|
25
|
+
title="🤖 llm"
|
|
26
|
+
body={preview}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function LoopNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
const summary = `over: ${step.over ?? '?'}\nas: ${step.as ?? 'item'}`;
|
|
9
|
+
return (
|
|
10
|
+
<BaseNode
|
|
11
|
+
{...props}
|
|
12
|
+
data={data}
|
|
13
|
+
variant="loop"
|
|
14
|
+
title="🔁 loop"
|
|
15
|
+
body={summary}
|
|
16
|
+
outputs={[
|
|
17
|
+
{ id: 'iter', label: 'iter' },
|
|
18
|
+
{ id: 'done', label: 'done' },
|
|
19
|
+
]}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function PauseNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
return (
|
|
9
|
+
<BaseNode
|
|
10
|
+
{...props}
|
|
11
|
+
data={data}
|
|
12
|
+
variant="pause"
|
|
13
|
+
title="⏸ pause"
|
|
14
|
+
body={step.prompt ?? '(human-in-the-loop)'}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function SkillNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
const lines = [
|
|
9
|
+
step.input ? `input: ${truncate(step.input, 40)}` : null,
|
|
10
|
+
step.maxTurns ? `maxTurns: ${step.maxTurns}` : null,
|
|
11
|
+
]
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.join('\n');
|
|
14
|
+
return (
|
|
15
|
+
<BaseNode
|
|
16
|
+
{...props}
|
|
17
|
+
data={data}
|
|
18
|
+
variant="skill"
|
|
19
|
+
title={`📚 ${step.skill}`}
|
|
20
|
+
body={lines || '(no input)'}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function truncate(s: string, n: number): string {
|
|
26
|
+
return s.length > n ? `${s.slice(0, n)}...` : s;
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import { BaseNode } from './BaseNode.tsx';
|
|
3
|
+
import type { CanvasNodeData } from '../../types.ts';
|
|
4
|
+
|
|
5
|
+
export function ToolNode(props: NodeProps) {
|
|
6
|
+
const data = props.data as CanvasNodeData;
|
|
7
|
+
const step = data.step;
|
|
8
|
+
const argsPreview = step.args
|
|
9
|
+
? Object.entries(step.args)
|
|
10
|
+
.slice(0, 2)
|
|
11
|
+
.map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 30)}`)
|
|
12
|
+
.join('\n')
|
|
13
|
+
: '(no args)';
|
|
14
|
+
return (
|
|
15
|
+
<BaseNode
|
|
16
|
+
{...props}
|
|
17
|
+
data={data}
|
|
18
|
+
variant="tool"
|
|
19
|
+
title={`🔧 ${step.tool}`}
|
|
20
|
+
body={argsPreview}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function truncate(s: string, n: number): string {
|
|
26
|
+
return s.length > n ? `${s.slice(0, n)}...` : s;
|
|
27
|
+
}
|