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.
Files changed (37) hide show
  1. package/index.html +12 -0
  2. package/package.json +44 -0
  3. package/public/manifest.fallback.json +482 -0
  4. package/scripts/cli.ts +318 -0
  5. package/scripts/server.ts +131 -0
  6. package/src/App.tsx +119 -0
  7. package/src/api/files.ts +32 -0
  8. package/src/api/manifest.ts +17 -0
  9. package/src/canvas/WorkflowCanvas.tsx +161 -0
  10. package/src/canvas/edges/SequentialEdge.tsx +14 -0
  11. package/src/canvas/nodes/AssertNode.tsx +17 -0
  12. package/src/canvas/nodes/BaseNode.tsx +55 -0
  13. package/src/canvas/nodes/BranchNode.tsx +21 -0
  14. package/src/canvas/nodes/LlmNode.tsx +29 -0
  15. package/src/canvas/nodes/LoopNode.tsx +22 -0
  16. package/src/canvas/nodes/PauseNode.tsx +17 -0
  17. package/src/canvas/nodes/SkillNode.tsx +27 -0
  18. package/src/canvas/nodes/ToolNode.tsx +27 -0
  19. package/src/index.css +379 -0
  20. package/src/inspector/Inspector.tsx +224 -0
  21. package/src/inspector/LlmNodeForm.tsx +59 -0
  22. package/src/inspector/SkillNodeForm.tsx +62 -0
  23. package/src/inspector/ToolNodeForm.tsx +120 -0
  24. package/src/main.tsx +10 -0
  25. package/src/palette/ControlFlowSection.tsx +45 -0
  26. package/src/palette/Palette.tsx +65 -0
  27. package/src/palette/SkillPaletteSection.tsx +31 -0
  28. package/src/palette/ToolPaletteSection.tsx +31 -0
  29. package/src/store/filesStore.ts +29 -0
  30. package/src/store/manifestStore.ts +25 -0
  31. package/src/store/workflowStore.ts +140 -0
  32. package/src/types.ts +83 -0
  33. package/src/utils/safePath.ts +49 -0
  34. package/src/yaml-view/YamlEditor.tsx +71 -0
  35. package/src/yaml-view/sync.ts +246 -0
  36. package/tsconfig.json +24 -0
  37. 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
+ }