@oneie/claude 0.6.0 → 0.7.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/agents/w1-recon.md +9 -4
- package/agents/w2-decide.md +51 -28
- package/agents/w3-edit.md +70 -78
- package/agents/w4-verify.md +164 -69
- package/commands/browser.md +98 -35
- package/commands/cc-connect.md +6 -0
- package/commands/chat.md +13 -1
- package/commands/close.md +45 -19
- package/commands/create.md +20 -10
- package/commands/db-sync.md +5 -1
- package/commands/deploy.md +174 -87
- package/commands/do-autonomous.md +31 -20
- package/commands/do-improve.md +4 -4
- package/commands/do-show.md +4 -3
- package/commands/do.md +53 -13
- package/commands/go.md +75 -0
- package/commands/improve.md +4 -2
- package/commands/kill.md +6 -8
- package/commands/notify.md +15 -2
- package/commands/oo-push.md +24 -9
- package/commands/rag.md +6 -0
- package/commands/release.md +46 -25
- package/commands/restart.md +5 -4
- package/commands/see.md +45 -35
- package/commands/skill-create.md +38 -5
- package/commands/sync.md +21 -11
- package/hooks/scripts/auto-continue.sh +70 -0
- package/hooks/scripts/branch-pin.sh +85 -0
- package/hooks/scripts/git-add-guard.sh +16 -5
- package/hooks/scripts/post-edit-check.sh +16 -0
- package/hooks/scripts/session-start.sh +13 -8
- package/hooks/scripts/sync-priority-todo.sh +5 -0
- package/package.json +1 -1
- package/rules/api.md +9 -0
- package/rules/design.md +13 -3
- package/rules/documentation.md +30 -71
- package/rules/engine.md +8 -1
- package/rules/ui.md +5 -1
- package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
- package/scripts/browser-check.mjs +44 -103
- package/scripts/cc-events-proof.sh +13 -12
- package/scripts/chrome.mjs +381 -0
- package/scripts/do-analyze.sh +109 -4
- package/scripts/do-auto.sh +350 -43
- package/scripts/do-consumer-sweep.sh +72 -0
- package/scripts/do-derives-check.sh +88 -0
- package/scripts/do-fleet.sh +50 -1
- package/scripts/do-killswitch-audit.py +33 -14
- package/scripts/do-promise-lint.sh +150 -4
- package/scripts/do-promise-settle.sh +131 -9
- package/scripts/do-prove.sh +197 -17
- package/scripts/do-rank.py +606 -20
- package/scripts/do-rubric.py +21 -2
- package/scripts/do-signal.sh +157 -17
- package/scripts/do-smoke.sh +9 -0
- package/scripts/do-substrate-check.sh +49 -0
- package/scripts/do-tasks-wire-check.sh +81 -0
- package/scripts/do-tier.sh +90 -5
- package/scripts/do-ui-gate.sh +332 -0
- package/scripts/do-untracked-gate.sh +231 -0
- package/scripts/do-walk.sh +344 -0
- package/scripts/factory-check.sh +747 -0
- package/scripts/factory-repo.sh +1361 -0
- package/scripts/factory-tasks-check.sh +85 -0
- package/scripts/outcome-pull.ts +279 -0
- package/scripts/promise-manifest.mjs +167 -0
- package/scripts/reconcile-allow.txt +5 -0
- package/scripts/rubric-weights.json +64 -0
- package/scripts/spine-canary.sh +20 -0
- package/scripts/typedb-cluster-status.sh +128 -0
- package/scripts/typedb-env.sh +182 -0
- package/scripts/typedb-flap-recorder.sh +85 -0
- package/scripts/typedb-probes/containment-probe.py +88 -0
- package/scripts/typedb-probes/panic-probe-version.py +92 -0
- package/scripts/typedb-probes/panic-probe.py +77 -0
- package/scripts/typedb-scratch.sh +73 -0
- package/scripts/urls-lint.sh +58 -0
- package/scripts/w4-rubric.ts +96 -12
- package/scripts/wf-check.mjs +75 -0
- package/skills/ai-ui/SKILL.md +1 -1
- package/skills/astro/SKILL.md +177 -132
- package/skills/astro/add-content-collection.md +29 -13
- package/skills/astro/check-build.md +19 -13
- package/skills/astro/create-component.md +36 -9
- package/skills/astro/create-page.md +39 -10
- package/skills/astro/optimize-performance.md +36 -8
- package/skills/cli/SKILL.md +254 -220
- package/skills/dev/SKILL.md +1 -1
- package/skills/directory-autofill/SKILL.md +68 -0
- package/skills/docs/SKILL.md +4 -4
- package/skills/mcp/SKILL.md +186 -156
- package/skills/promise-make/SKILL.md +2 -2
- package/skills/puck/SKILL.md +465 -250
- package/skills/react19/SKILL.md +196 -296
- package/skills/reactflow/SKILL.md +387 -412
- package/skills/sdk/SKILL.md +167 -106
- package/skills/shadcn/SKILL.md +245 -201
- package/skills/signal/SKILL.md +5 -0
- package/skills/sui/SKILL.md +6 -1
- package/skills/tutorial/SKILL.md +15 -0
- package/skills/typedb/SKILL.md +427 -217
- package/skills/typedb/examples/python-patterns.py +11 -4
- package/skills/typedb/examples/query-patterns.tql +8 -3
- package/skills/typedb/examples/schema-patterns.tql +4 -2
- package/skills/typedb/reference/migration-2x-3x.md +19 -1
- package/skills/typedb/reference/python-driver.md +17 -1
- package/skills/voice/SKILL.md +117 -0
- package/skills/writer/SKILL.md +29 -12
- package/templates/template-feature.md +26 -1
- package/templates/template-tests.md +4 -0
- package/templates/template-todo.md +65 -10
|
@@ -1,47 +1,74 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reactflow
|
|
3
|
-
description:
|
|
3
|
+
description: Node-based graph surfaces in `one.ie/web` built on @xyflow/react v12 — WorkflowFlow, OrgChartView, SignalGraph, LifecycleFlow, PathGraph, TypesCanvas. Covers the v12 NodeProps/EdgeProps generic pattern the repo actually uses, node/edge type registries, 6-token styling inside a canvas, dagre auto-layout, and client:only mounting. Use when adding a custom node or edge, wiring a nodeTypes registry, fixing a graph that renders unstyled or fails tsc, or laying out substrate paths.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# ReactFlow Development
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Interactive graph surfaces in `one.ie/web`, built on `@xyflow/react` **12.10.2**
|
|
11
|
+
(declared `^12.10.2`).
|
|
12
|
+
|
|
13
|
+
This is a live, heavily-used dependency — **25 files** under
|
|
14
|
+
`one.ie/web/src/components/` import it. Read one of them before writing a new
|
|
15
|
+
graph; the conventions below are extracted from them, not from upstream docs.
|
|
16
|
+
|
|
17
|
+
| Surface | Entry | Node registry |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Workflow canvas | `components/workflows/WorkflowFlow.tsx` | `NODE_TYPES` — 8 step kinds + `chain` + `route` |
|
|
20
|
+
| Org chart | `components/org/OrgChartView.tsx` | `{ role: RoleNode }` |
|
|
21
|
+
| Signal graph | `components/org/SignalGraph.tsx` | `{ role: RoleNode }` · `{ path: PathEdge }` |
|
|
22
|
+
| Lifecycle | `components/lifecycle/LifecycleFlow.tsx` | 8 node types · `{ lifecycle: LifecycleEdge }` |
|
|
23
|
+
| Paths | `components/paths/PathGraph.tsx` + `path-flow.ts` | substrate strength/resistance |
|
|
24
|
+
| Types canvas | `components/data/TypesCanvas.tsx` | dagre auto-layout |
|
|
25
|
+
| Router brain | `components/do/RouterBrain.tsx` | `{ shape, model }` · `{ 'signal-edge': SignalEdge }` |
|
|
26
|
+
| AI canvas | `components/ai-elements/{canvas,node,edge,controls}.tsx` | generic primitives |
|
|
11
27
|
|
|
12
28
|
## Works With
|
|
13
29
|
|
|
14
30
|
| Skill | Load when |
|
|
15
31
|
|------------|----------------------------------------------------------------------------------------------|
|
|
16
32
|
| `/react19` | Custom nodes/edges are React components — ref-as-prop, transitions for non-blocking updates. |
|
|
17
|
-
| `/typedb` | Edge weights render `path.strength
|
|
18
|
-
| `/
|
|
19
|
-
| `/
|
|
20
|
-
| `/
|
|
33
|
+
| `/typedb` | Edge weights render `path.strength` / `path.resistance`; query highways for layout. |
|
|
34
|
+
| `/astro` | Graph pages are Astro routes — mount `client:only="react"` (the canvas cannot SSR). |
|
|
35
|
+
| `/shadcn` | Side panels, node detail sheets, toolbars. |
|
|
36
|
+
| `/puck` | `OrgChartView` and `WorkflowFlow` are lazy-loaded as Puck blocks in `lib/puck/config.tsx`. |
|
|
37
|
+
|
|
38
|
+
Every node/edge click emits `emitClick('ui:<surface>:<action>')` from
|
|
39
|
+
`@/lib/ui-signal` — see `.claude/rules/ui.md`. `rules/design.md` auto-loads on
|
|
40
|
+
these files too: a canvas is not exempt from the 6-token system.
|
|
21
41
|
|
|
22
42
|
## When to Use This Skill
|
|
23
43
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
44
|
+
- Add a custom node or edge type to an existing canvas
|
|
45
|
+
- Build a new graph surface over substrate data
|
|
46
|
+
- Fix a node component that fails `tsc` on `NodeProps`
|
|
47
|
+
- Lay out a graph with dagre or a hand-rolled tier layout
|
|
48
|
+
- Style a canvas so it follows the workspace brand tokens
|
|
29
49
|
|
|
30
50
|
## Installation
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
Already installed — do not re-add:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
@xyflow/react ^12.10.2 (one.ie/web/package.json)
|
|
56
|
+
@dagrejs/dagre ^3.0.0 (used by components/data/TypesCanvas.tsx)
|
|
34
57
|
```
|
|
35
58
|
|
|
36
59
|
## Core Setup
|
|
37
60
|
|
|
61
|
+
Register node and edge types as **module-level constants**. Defining them inside
|
|
62
|
+
the component body creates a new object identity every render and ReactFlow
|
|
63
|
+
remounts every node.
|
|
64
|
+
|
|
38
65
|
```tsx
|
|
39
|
-
// src/components/flow/FlowCanvas.tsx
|
|
40
66
|
import {
|
|
41
67
|
ReactFlow,
|
|
42
68
|
Background,
|
|
43
69
|
Controls,
|
|
44
70
|
MiniMap,
|
|
71
|
+
Panel,
|
|
45
72
|
useNodesState,
|
|
46
73
|
useEdgesState,
|
|
47
74
|
type Node,
|
|
@@ -49,318 +76,274 @@ import {
|
|
|
49
76
|
} from '@xyflow/react';
|
|
50
77
|
import '@xyflow/react/dist/style.css';
|
|
51
78
|
|
|
79
|
+
import { TriggerNode } from './nodes/TriggerNode';
|
|
52
80
|
import { AgentNode } from './nodes/AgentNode';
|
|
53
|
-
import { EnvelopeNode } from './nodes/EnvelopeNode';
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
const nodeTypes = {
|
|
57
|
-
agent: AgentNode,
|
|
58
|
-
envelope: EnvelopeNode,
|
|
59
|
-
};
|
|
82
|
+
const NODE_TYPES = { trigger: TriggerNode, agent: AgentNode };
|
|
60
83
|
|
|
61
|
-
export function FlowCanvas(
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
export function FlowCanvas({ initialNodes, initialEdges }: {
|
|
85
|
+
initialNodes: Node[];
|
|
86
|
+
initialEdges: Edge[];
|
|
87
|
+
}) {
|
|
88
|
+
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
|
89
|
+
const [edges, , onEdgesChange] = useEdgesState(initialEdges);
|
|
64
90
|
|
|
65
91
|
return (
|
|
66
|
-
<div className="h-full w-full bg-
|
|
92
|
+
<div className="h-full w-full bg-background">
|
|
67
93
|
<ReactFlow
|
|
68
94
|
nodes={nodes}
|
|
69
95
|
edges={edges}
|
|
70
96
|
onNodesChange={onNodesChange}
|
|
71
97
|
onEdgesChange={onEdgesChange}
|
|
72
|
-
nodeTypes={
|
|
98
|
+
nodeTypes={NODE_TYPES}
|
|
73
99
|
fitView
|
|
74
|
-
|
|
100
|
+
panOnScroll
|
|
101
|
+
proOptions={{ hideAttribution: true }}
|
|
75
102
|
>
|
|
76
|
-
<Background
|
|
77
|
-
<Controls
|
|
78
|
-
<MiniMap
|
|
79
|
-
nodeColor={(node) => {
|
|
80
|
-
if (node.type === 'agent') return '#3b82f6';
|
|
81
|
-
return '#22c55e';
|
|
82
|
-
}}
|
|
83
|
-
className="bg-[#0f0f14] border-[#1e293b]"
|
|
84
|
-
/>
|
|
103
|
+
<Background bgColor="var(--sidebar)" />
|
|
104
|
+
<Controls showInteractive={false} />
|
|
105
|
+
<MiniMap pannable zoomable />
|
|
85
106
|
</ReactFlow>
|
|
86
107
|
</div>
|
|
87
108
|
);
|
|
88
109
|
}
|
|
89
110
|
```
|
|
90
111
|
|
|
112
|
+
`proOptions={{ hideAttribution: true }}` is on every canvas in the repo.
|
|
113
|
+
`<Background>` takes `bgColor` as a CSS value — pass a token var, never a hex.
|
|
114
|
+
|
|
91
115
|
## Custom Nodes
|
|
92
116
|
|
|
93
|
-
###
|
|
117
|
+
### The v12 typing pattern
|
|
118
|
+
|
|
119
|
+
`NodeProps` in v12 is **not** generic over your data interface. `NodeProps<T>`
|
|
120
|
+
expects a `Node` type, so `NodeProps<AgentNodeData>` fails to compile. Every node
|
|
121
|
+
in this repo uses the same intersection instead:
|
|
94
122
|
|
|
95
123
|
```tsx
|
|
96
|
-
// src/components/flow/nodes/AgentNode.tsx
|
|
97
124
|
import { Handle, Position, type NodeProps } from '@xyflow/react';
|
|
98
|
-
import { Badge } from '@/components/ui/badge';
|
|
99
125
|
|
|
100
|
-
interface
|
|
126
|
+
export interface StepNodeData {
|
|
101
127
|
name: string;
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
kind: string;
|
|
129
|
+
paused?: boolean;
|
|
130
|
+
[key: string]: unknown; // required — v12 constrains node data to Record<string, unknown>
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
waiting: 'bg-amber-500 animate-pulse',
|
|
109
|
-
idle: 'bg-slate-500',
|
|
110
|
-
error: 'bg-red-500',
|
|
111
|
-
};
|
|
133
|
+
export type StepNodeProps = Omit<NodeProps, 'data'> & { data: StepNodeData };
|
|
134
|
+
```
|
|
112
135
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
{/* Output handle */}
|
|
134
|
-
<Handle
|
|
135
|
-
type="source"
|
|
136
|
-
position={Position.Right}
|
|
137
|
-
className="!bg-green-500 !w-3 !h-3"
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
136
|
+
The `[key: string]: unknown` index signature is mandatory. Without it TypeScript
|
|
137
|
+
rejects the type when ReactFlow assigns it into `Node['data']`.
|
|
138
|
+
|
|
139
|
+
For edges the equivalent is an interface extension:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import type { EdgeProps } from '@xyflow/react';
|
|
143
|
+
|
|
144
|
+
export interface SignalEdgeData {
|
|
145
|
+
strength: number;
|
|
146
|
+
resistance: number;
|
|
147
|
+
traversals: number;
|
|
148
|
+
isHighway?: boolean;
|
|
149
|
+
[key: string]: unknown;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface Props extends EdgeProps {
|
|
153
|
+
data: SignalEdgeData;
|
|
141
154
|
}
|
|
142
155
|
```
|
|
143
156
|
|
|
144
|
-
|
|
157
|
+
`data` can arrive undefined on a freshly-connected edge — every real edge in the
|
|
158
|
+
repo defaults it: `const d = data ?? { strength: 1, resistance: 0, traversals: 0 }`.
|
|
159
|
+
|
|
160
|
+
### A step node
|
|
161
|
+
|
|
162
|
+
Nodes share a presentational shell so the card, badge, and handle styling live in
|
|
163
|
+
one place. `components/workflows/nodes/StepNodeShell.tsx` is that shell; the eight
|
|
164
|
+
kind nodes are ~8 lines each.
|
|
145
165
|
|
|
146
166
|
```tsx
|
|
147
|
-
//
|
|
148
|
-
import {
|
|
149
|
-
import {
|
|
167
|
+
// components/workflows/nodes/TriggerNode.tsx — the whole file
|
|
168
|
+
import { STEP_VISUAL } from '../step-visual';
|
|
169
|
+
import { StepNodeShell, type StepNodeProps } from './StepNodeShell';
|
|
170
|
+
|
|
171
|
+
const { icon, tone, label } = STEP_VISUAL.trigger;
|
|
150
172
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
status: 'pending' | 'resolved' | 'rejected';
|
|
155
|
-
hasCallback: boolean;
|
|
173
|
+
/** Trigger — the workflow's entry point. No inbound handle (nothing precedes it). */
|
|
174
|
+
export function TriggerNode({ data }: StepNodeProps) {
|
|
175
|
+
return <StepNodeShell icon={icon} label={label} accent={tone} data={data} hasTarget={false} />;
|
|
156
176
|
}
|
|
177
|
+
```
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
pending: 'border-amber-500/50 bg-amber-500/10',
|
|
160
|
-
resolved: 'border-green-500/50 bg-green-500/10',
|
|
161
|
-
rejected: 'border-red-500/50 bg-red-500/10',
|
|
162
|
-
};
|
|
179
|
+
The shell renders a token-styled card and a shared `StepHandle`:
|
|
163
180
|
|
|
164
|
-
|
|
181
|
+
```tsx
|
|
182
|
+
import { Handle, Position } from '@xyflow/react';
|
|
183
|
+
import type { LucideIcon } from 'lucide-react';
|
|
184
|
+
|
|
185
|
+
export function StepHandle({ type, position, id, color }: {
|
|
186
|
+
type: 'source' | 'target';
|
|
187
|
+
position: Position;
|
|
188
|
+
id?: string;
|
|
189
|
+
color: string;
|
|
190
|
+
}) {
|
|
191
|
+
const edge = position === Position.Top ? { top: -5 } : { bottom: -5 };
|
|
165
192
|
return (
|
|
166
|
-
<
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
<div className="font-mono text-sm text-green-400">
|
|
181
|
-
{data.action}
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
<Badge
|
|
185
|
-
variant="outline"
|
|
186
|
-
className={`mt-2 ${
|
|
187
|
-
data.status === 'resolved' ? 'text-green-400' :
|
|
188
|
-
data.status === 'pending' ? 'text-amber-400' : 'text-red-400'
|
|
189
|
-
}`}
|
|
190
|
-
>
|
|
191
|
-
{data.status}
|
|
192
|
-
</Badge>
|
|
193
|
-
|
|
194
|
-
{/* Output handle (only if has callback) */}
|
|
195
|
-
{data.hasCallback && (
|
|
196
|
-
<Handle
|
|
197
|
-
type="source"
|
|
198
|
-
position={Position.Right}
|
|
199
|
-
className="!bg-green-500 !w-3 !h-3"
|
|
200
|
-
/>
|
|
201
|
-
)}
|
|
202
|
-
</div>
|
|
193
|
+
<Handle
|
|
194
|
+
type={type}
|
|
195
|
+
id={id}
|
|
196
|
+
position={position}
|
|
197
|
+
style={{
|
|
198
|
+
width: 9,
|
|
199
|
+
height: 9,
|
|
200
|
+
background: color,
|
|
201
|
+
border: '2px solid var(--color-background)',
|
|
202
|
+
...edge,
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
```
|
|
206
208
|
|
|
207
|
-
|
|
209
|
+
Node colors come from the token vars, resolved from an accent name. The shell
|
|
210
|
+
receives the lucide component as a prop and renames it to a capitalized local so
|
|
211
|
+
JSX treats it as a component, not an intrinsic element:
|
|
208
212
|
|
|
209
213
|
```tsx
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
// inside StepNodeShell({ icon: Icon, accent, ... }: { icon: LucideIcon; accent: Accent; ... })
|
|
215
|
+
type Accent = 'primary' | 'secondary' | 'tertiary' | 'font';
|
|
216
|
+
const color = `var(--color-${accent})`;
|
|
217
|
+
|
|
218
|
+
<article
|
|
219
|
+
className="bg-background border rounded-2xl w-[200px] flex flex-col relative"
|
|
220
|
+
style={{ borderColor: 'var(--color-border)', boxShadow: 'var(--shadow-card)' }}
|
|
221
|
+
>
|
|
222
|
+
<span style={{ background: `color-mix(in oklab, ${color} 18%, transparent)`, color }}>
|
|
223
|
+
<Icon size={15} strokeWidth={1.75} />
|
|
224
|
+
</span>
|
|
225
|
+
</article>
|
|
226
|
+
```
|
|
214
227
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
228
|
+
Never write a hex or a Tailwind palette class inside a node. `PathGraph.tsx`
|
|
229
|
+
still carries four hex literals (lines 27–29, 55) — it predates the design hook
|
|
230
|
+
and is a known offender, not a pattern to copy.
|
|
219
231
|
|
|
220
|
-
|
|
221
|
-
const nodes: Node[] = [];
|
|
222
|
-
const edges: Edge[] = [];
|
|
223
|
-
|
|
224
|
-
const agents = Array.from(runtime.router.agents.values());
|
|
225
|
-
const allEnvelopes = agents.flatMap(a => a.envelopes);
|
|
226
|
-
|
|
227
|
-
// Position agents in a row at the top
|
|
228
|
-
agents.forEach((agent, index) => {
|
|
229
|
-
nodes.push({
|
|
230
|
-
id: agent.id,
|
|
231
|
-
type: 'agent',
|
|
232
|
-
position: { x: index * 250, y: 0 },
|
|
233
|
-
data: {
|
|
234
|
-
name: agent.name,
|
|
235
|
-
status: agent.status,
|
|
236
|
-
actionCount: Object.keys(agent.actions).length,
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
});
|
|
232
|
+
## Building the graph from substrate data
|
|
240
233
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
id: `${envelope.id}-${envelope.metadata.receiver}`,
|
|
276
|
-
source: envelope.id,
|
|
277
|
-
target: envelope.metadata.receiver,
|
|
278
|
-
animated: envelope.payload.status === 'pending',
|
|
279
|
-
style: { stroke: '#22c55e' },
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Edge to callback envelope
|
|
284
|
-
if (envelope.callback) {
|
|
285
|
-
edges.push({
|
|
286
|
-
id: `${envelope.id}-callback`,
|
|
287
|
-
source: envelope.id,
|
|
288
|
-
target: envelope.callback.id,
|
|
289
|
-
animated: true,
|
|
290
|
-
style: { stroke: '#eab308', strokeDasharray: '5,5' },
|
|
291
|
-
label: 'callback',
|
|
292
|
-
labelStyle: { fill: '#eab308', fontSize: 10 },
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
});
|
|
234
|
+
Graph data is derived in a **pure module** with no React and no xyflow runtime
|
|
235
|
+
import, so it is unit-testable. `components/paths/path-flow.ts` is the reference:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
239
|
+
|
|
240
|
+
export interface PathRow {
|
|
241
|
+
from: string;
|
|
242
|
+
to: string;
|
|
243
|
+
fromName?: string;
|
|
244
|
+
toName?: string;
|
|
245
|
+
strength: number;
|
|
246
|
+
resistance: number;
|
|
247
|
+
traversals: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function toFlow(paths: PathRow[]): { nodes: Node[]; edges: Edge[] } {
|
|
251
|
+
const actors = [...new Set(paths.flatMap(p => [p.from, p.to]))];
|
|
252
|
+
|
|
253
|
+
const nodes: Node[] = actors.map((id, i) => ({
|
|
254
|
+
id,
|
|
255
|
+
type: 'role',
|
|
256
|
+
position: { x: (i % 4) * 260, y: Math.floor(i / 4) * 200 },
|
|
257
|
+
data: { name: id },
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
const edges: Edge[] = paths.map(p => ({
|
|
261
|
+
id: `${p.from}-${p.to}`,
|
|
262
|
+
source: p.from,
|
|
263
|
+
target: p.to,
|
|
264
|
+
type: 'path',
|
|
265
|
+
animated: p.traversals > 0,
|
|
266
|
+
data: { strength: p.strength, resistance: p.resistance, traversals: p.traversals },
|
|
267
|
+
}));
|
|
296
268
|
|
|
297
269
|
return { nodes, edges };
|
|
298
270
|
}
|
|
299
271
|
```
|
|
300
272
|
|
|
273
|
+
Edge weight maps to stroke width and token color — the visual encoding of
|
|
274
|
+
`strength` vs `resistance`, straight from `components/org/SignalEdge.tsx`:
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
function strokeWidth(strength: number): number {
|
|
278
|
+
return Math.max(1, Math.min(8, Math.log2(strength + 1) * 2));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function strokeColor(data: SignalEdgeData): string {
|
|
282
|
+
if (data.isHighway) return 'var(--color-tertiary)';
|
|
283
|
+
if (data.resistance > data.strength) return 'var(--color-secondary)';
|
|
284
|
+
return 'var(--color-primary)';
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
301
288
|
## Interactive Flow Canvas
|
|
302
289
|
|
|
290
|
+
An editable canvas adds `onConnect`, a click handler, and gating props. Note the
|
|
291
|
+
handler signature: `onNodeClick` receives `(event, node)` and `node` must be
|
|
292
|
+
imported as a type — omitting the import is the most common tsc failure here.
|
|
293
|
+
|
|
303
294
|
```tsx
|
|
304
|
-
|
|
305
|
-
import { useCallback, useEffect } from 'react';
|
|
295
|
+
import { useCallback } from 'react';
|
|
306
296
|
import {
|
|
307
297
|
ReactFlow,
|
|
308
298
|
Background,
|
|
309
299
|
Controls,
|
|
310
300
|
MiniMap,
|
|
301
|
+
Panel,
|
|
302
|
+
addEdge,
|
|
311
303
|
useNodesState,
|
|
312
304
|
useEdgesState,
|
|
313
|
-
addEdge,
|
|
314
305
|
type Connection,
|
|
306
|
+
type Node,
|
|
307
|
+
type Edge,
|
|
315
308
|
} from '@xyflow/react';
|
|
316
309
|
import '@xyflow/react/dist/style.css';
|
|
317
|
-
|
|
310
|
+
import { emitClick } from '@/lib/ui-signal';
|
|
311
|
+
import { TriggerNode } from './nodes/TriggerNode';
|
|
318
312
|
import { AgentNode } from './nodes/AgentNode';
|
|
319
|
-
import {
|
|
320
|
-
import { runtimeToFlow } from './utils/runtimeToFlow';
|
|
321
|
-
import type { Runtime } from '@/engine/Runtime';
|
|
313
|
+
import { PathEdge } from './PathEdge';
|
|
322
314
|
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
envelope: EnvelopeNode,
|
|
326
|
-
};
|
|
315
|
+
const NODE_TYPES = { trigger: TriggerNode, agent: AgentNode };
|
|
316
|
+
const EDGE_TYPES = { path: PathEdge };
|
|
327
317
|
|
|
328
318
|
interface Props {
|
|
329
|
-
|
|
330
|
-
|
|
319
|
+
initialNodes: Node[];
|
|
320
|
+
initialEdges: Edge[];
|
|
321
|
+
editable?: boolean;
|
|
322
|
+
onNodeOpen?: (id: string) => void;
|
|
331
323
|
}
|
|
332
324
|
|
|
333
|
-
export function InteractiveFlowCanvas({
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
setEdges(newEdges);
|
|
342
|
-
}, [runtime, setNodes, setEdges]);
|
|
325
|
+
export function InteractiveFlowCanvas({
|
|
326
|
+
initialNodes,
|
|
327
|
+
initialEdges,
|
|
328
|
+
editable = false,
|
|
329
|
+
onNodeOpen,
|
|
330
|
+
}: Props) {
|
|
331
|
+
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
|
332
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
343
333
|
|
|
344
|
-
// Handle new connections
|
|
345
334
|
const onConnect = useCallback(
|
|
346
335
|
(connection: Connection) => {
|
|
347
|
-
setEdges(
|
|
348
|
-
...connection,
|
|
349
|
-
animated: true,
|
|
350
|
-
style: { stroke: '#3b82f6' },
|
|
351
|
-
}, eds));
|
|
336
|
+
setEdges(eds => addEdge({ ...connection, animated: true }, eds));
|
|
352
337
|
},
|
|
353
338
|
[setEdges]
|
|
354
339
|
);
|
|
355
340
|
|
|
356
|
-
// Handle node clicks
|
|
357
341
|
const onNodeClick = useCallback(
|
|
358
342
|
(_: React.MouseEvent, node: Node) => {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
343
|
+
emitClick('ui:graph:open', { id: node.id });
|
|
344
|
+
onNodeOpen?.(node.id);
|
|
362
345
|
},
|
|
363
|
-
[
|
|
346
|
+
[onNodeOpen]
|
|
364
347
|
);
|
|
365
348
|
|
|
366
349
|
return (
|
|
@@ -372,241 +355,233 @@ export function InteractiveFlowCanvas({ runtime, onEnvelopeClick }: Props) {
|
|
|
372
355
|
onEdgesChange={onEdgesChange}
|
|
373
356
|
onConnect={onConnect}
|
|
374
357
|
onNodeClick={onNodeClick}
|
|
375
|
-
nodeTypes={
|
|
358
|
+
nodeTypes={NODE_TYPES}
|
|
359
|
+
edgeTypes={EDGE_TYPES}
|
|
360
|
+
nodesDraggable={editable}
|
|
361
|
+
nodesConnectable={editable}
|
|
362
|
+
edgesReconnectable={false}
|
|
363
|
+
deleteKeyCode={editable ? ['Backspace', 'Delete'] : null}
|
|
376
364
|
fitView
|
|
377
|
-
|
|
378
|
-
|
|
365
|
+
panOnScroll
|
|
366
|
+
proOptions={{ hideAttribution: true }}
|
|
379
367
|
>
|
|
380
|
-
<Background
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
/>
|
|
385
|
-
<Controls
|
|
386
|
-
showZoom
|
|
387
|
-
showFitView
|
|
388
|
-
showInteractive
|
|
389
|
-
className="!bg-[#0f0f14] !border-[#1e293b] !shadow-lg"
|
|
390
|
-
/>
|
|
391
|
-
<MiniMap
|
|
392
|
-
nodeColor={(node) => {
|
|
393
|
-
if (node.type === 'agent') return '#3b82f6';
|
|
394
|
-
if (node.data?.status === 'resolved') return '#22c55e';
|
|
395
|
-
if (node.data?.status === 'pending') return '#eab308';
|
|
396
|
-
return '#64748b';
|
|
397
|
-
}}
|
|
398
|
-
className="!bg-[#0f0f14] !border-[#1e293b]"
|
|
399
|
-
maskColor="rgba(10, 10, 15, 0.8)"
|
|
400
|
-
/>
|
|
368
|
+
<Background bgColor="var(--sidebar)" />
|
|
369
|
+
<MiniMap pannable zoomable />
|
|
370
|
+
<Controls showInteractive={false} />
|
|
371
|
+
{editable && <Panel position="top-left">{/* toolbar */}</Panel>}
|
|
401
372
|
</ReactFlow>
|
|
402
373
|
</div>
|
|
403
374
|
);
|
|
404
375
|
}
|
|
405
376
|
```
|
|
406
377
|
|
|
378
|
+
`deleteKeyCode={null}` is how a read-only canvas blocks node deletion — there is
|
|
379
|
+
no `deletable` prop on `<ReactFlow>`.
|
|
380
|
+
|
|
407
381
|
## Custom Edges
|
|
408
382
|
|
|
383
|
+
`LifecycleFlow.tsx` picks its path function from edge data and animates a dot
|
|
384
|
+
along it with SVG `animateMotion`:
|
|
385
|
+
|
|
409
386
|
```tsx
|
|
410
|
-
// src/components/flow/edges/CallbackEdge.tsx
|
|
411
387
|
import {
|
|
412
388
|
BaseEdge,
|
|
413
389
|
EdgeLabelRenderer,
|
|
414
390
|
getBezierPath,
|
|
391
|
+
getStraightPath,
|
|
415
392
|
type EdgeProps,
|
|
416
393
|
} from '@xyflow/react';
|
|
417
394
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
markerEnd,
|
|
395
|
+
interface EdgeData extends Record<string, unknown> {
|
|
396
|
+
kind: 'forward' | 'loop';
|
|
397
|
+
flow?: boolean;
|
|
398
|
+
showLabel?: boolean;
|
|
399
|
+
label?: string;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function LifecycleEdge({
|
|
403
|
+
sourceX, sourceY, targetX, targetY,
|
|
404
|
+
sourcePosition, targetPosition, markerEnd, style, data,
|
|
428
405
|
}: EdgeProps) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
targetY,
|
|
435
|
-
targetPosition,
|
|
436
|
-
});
|
|
406
|
+
const d = data as EdgeData | undefined;
|
|
407
|
+
const [path, labelX, labelY] =
|
|
408
|
+
d?.kind === 'forward'
|
|
409
|
+
? getStraightPath({ sourceX, sourceY, targetX, targetY })
|
|
410
|
+
: getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
|
|
437
411
|
|
|
438
412
|
return (
|
|
439
413
|
<>
|
|
440
|
-
<BaseEdge
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
</EdgeLabelRenderer>
|
|
414
|
+
<BaseEdge path={path} markerEnd={markerEnd} style={style} />
|
|
415
|
+
{d?.flow && (
|
|
416
|
+
<circle r={3.5} fill="var(--color-primary)">
|
|
417
|
+
<animateMotion dur="2.4s" repeatCount="indefinite" path={path} />
|
|
418
|
+
</circle>
|
|
419
|
+
)}
|
|
420
|
+
{d?.showLabel && d?.label && (
|
|
421
|
+
<EdgeLabelRenderer>
|
|
422
|
+
<div
|
|
423
|
+
className="pointer-events-none px-2 py-0.5 rounded-full text-xs font-semibold bg-background border text-font/80 whitespace-nowrap"
|
|
424
|
+
style={{
|
|
425
|
+
position: 'absolute',
|
|
426
|
+
transform: `translate(-50%,-50%) translate(${labelX}px,${labelY}px)`,
|
|
427
|
+
borderColor: 'var(--color-border)',
|
|
428
|
+
boxShadow: 'var(--shadow-card)',
|
|
429
|
+
}}
|
|
430
|
+
>
|
|
431
|
+
{d.label}
|
|
432
|
+
</div>
|
|
433
|
+
</EdgeLabelRenderer>
|
|
434
|
+
)}
|
|
462
435
|
</>
|
|
463
436
|
);
|
|
464
437
|
}
|
|
465
438
|
```
|
|
466
439
|
|
|
440
|
+
`getBezierPath` needs `sourcePosition` and `targetPosition`; `getStraightPath`
|
|
441
|
+
takes only the four coordinates. Passing the extra keys to `getStraightPath` is
|
|
442
|
+
harmless but passing too few to `getBezierPath` yields a degenerate path.
|
|
443
|
+
|
|
467
444
|
## Edge Types Registration
|
|
468
445
|
|
|
469
446
|
```tsx
|
|
470
|
-
import {
|
|
447
|
+
import { LifecycleEdge } from './LifecycleEdge';
|
|
471
448
|
|
|
472
|
-
const
|
|
473
|
-
callback: CallbackEdge,
|
|
474
|
-
};
|
|
449
|
+
const EDGE_TYPES = { lifecycle: LifecycleEdge };
|
|
475
450
|
|
|
476
|
-
|
|
477
|
-
<ReactFlow
|
|
478
|
-
edgeTypes={edgeTypes}
|
|
479
|
-
// ...
|
|
480
|
-
/>
|
|
451
|
+
<ReactFlow edgeTypes={EDGE_TYPES} /* ... */ />
|
|
481
452
|
```
|
|
482
453
|
|
|
454
|
+
Module scope, same as `NODE_TYPES`. An edge opts in via `type: 'lifecycle'` in
|
|
455
|
+
its `Edge` object.
|
|
456
|
+
|
|
483
457
|
## Auto-Layout with dagre
|
|
484
458
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
459
|
+
`components/data/TypesCanvas.tsx` is the only dagre consumer. It falls back to a
|
|
460
|
+
grid when there are no edges — dagre on a disconnected graph stacks everything at
|
|
461
|
+
the origin:
|
|
488
462
|
|
|
489
|
-
```
|
|
490
|
-
// src/components/flow/utils/autoLayout.ts
|
|
463
|
+
```ts
|
|
491
464
|
import dagre from '@dagrejs/dagre';
|
|
492
465
|
import type { Node, Edge } from '@xyflow/react';
|
|
493
466
|
|
|
494
|
-
const
|
|
495
|
-
|
|
467
|
+
const NODE_WIDTH = 240;
|
|
468
|
+
const NODE_HEIGHT = 160;
|
|
469
|
+
|
|
470
|
+
function dagreLayout(nodes: Node[], edges: Edge[]): Node[] {
|
|
471
|
+
const g = new dagre.graphlib.Graph();
|
|
472
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
473
|
+
g.setGraph({ rankdir: 'LR', ranksep: 120, nodesep: 60 });
|
|
474
|
+
for (const node of nodes) g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
475
|
+
for (const edge of edges) g.setEdge(edge.source, edge.target);
|
|
476
|
+
dagre.layout(g);
|
|
477
|
+
return nodes.map(node => {
|
|
478
|
+
const { x, y } = g.node(node.id);
|
|
479
|
+
return { ...node, position: { x: x - NODE_WIDTH / 2, y: y - NODE_HEIGHT / 2 } };
|
|
480
|
+
});
|
|
481
|
+
}
|
|
496
482
|
|
|
497
|
-
const
|
|
498
|
-
const nodeHeight = 100;
|
|
483
|
+
const GRID_COLS = 3;
|
|
499
484
|
|
|
500
|
-
|
|
501
|
-
nodes
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
)
|
|
505
|
-
|
|
485
|
+
function gridLayout(nodes: Node[]): Node[] {
|
|
486
|
+
return nodes.map((node, i) => ({
|
|
487
|
+
...node,
|
|
488
|
+
position: { x: (i % GRID_COLS) * 300 + 32, y: Math.floor(i / GRID_COLS) * 220 + 32 },
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
506
491
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
492
|
+
export function layoutNodes(nodes: Node[], edges: Edge[]): Node[] {
|
|
493
|
+
return edges.length === 0 ? gridLayout(nodes) : dagreLayout(nodes, edges);
|
|
494
|
+
}
|
|
495
|
+
```
|
|
510
496
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
});
|
|
497
|
+
Build the graph fresh per call — a module-level `dagre.graphlib.Graph()` reused
|
|
498
|
+
across renders accumulates stale nodes.
|
|
514
499
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const layoutedNodes = nodes.map((node) => {
|
|
518
|
-
const nodeWithPosition = dagreGraph.node(node.id);
|
|
519
|
-
return {
|
|
520
|
-
...node,
|
|
521
|
-
position: {
|
|
522
|
-
x: nodeWithPosition.x - nodeWidth / 2,
|
|
523
|
-
y: nodeWithPosition.y - nodeHeight / 2,
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
});
|
|
500
|
+
For a known hierarchy, skip dagre. `OrgChartView` and `SignalGraph` place tiers
|
|
501
|
+
on fixed rows:
|
|
527
502
|
|
|
528
|
-
|
|
529
|
-
|
|
503
|
+
```ts
|
|
504
|
+
const ROW_Y: Record<Tier, number> = {
|
|
505
|
+
chairman: 0, ceo: 260, director: 560, specialist: 880,
|
|
506
|
+
};
|
|
530
507
|
```
|
|
531
508
|
|
|
532
509
|
## Astro Page with ReactFlow
|
|
533
510
|
|
|
511
|
+
Always `client:only="react"` — the canvas measures the DOM and cannot SSR.
|
|
512
|
+
`client:load` renders an empty container on the server and flashes.
|
|
513
|
+
|
|
534
514
|
```astro
|
|
535
515
|
---
|
|
536
|
-
// src/pages/
|
|
516
|
+
// src/pages/org/index.astro
|
|
517
|
+
export const prerender = false;
|
|
537
518
|
import Layout from "@/layouts/Layout.astro";
|
|
538
|
-
import {
|
|
519
|
+
import { OrgChartView } from "@/components/org/OrgChartView";
|
|
539
520
|
---
|
|
540
521
|
|
|
541
|
-
<Layout title="
|
|
522
|
+
<Layout title="Org chart">
|
|
542
523
|
<div class="h-screen">
|
|
543
|
-
<
|
|
524
|
+
<OrgChartView chart="complete" client:only="react" />
|
|
544
525
|
</div>
|
|
545
526
|
</Layout>
|
|
546
|
-
|
|
547
|
-
<style>
|
|
548
|
-
/* ReactFlow dark theme overrides */
|
|
549
|
-
:global(.react-flow__node) {
|
|
550
|
-
font-family: ui-monospace, monospace;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
:global(.react-flow__controls button) {
|
|
554
|
-
background-color: #0f0f14 !important;
|
|
555
|
-
border-color: #1e293b !important;
|
|
556
|
-
color: white !important;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
:global(.react-flow__controls button:hover) {
|
|
560
|
-
background-color: #1e293b !important;
|
|
561
|
-
}
|
|
562
|
-
</style>
|
|
563
527
|
```
|
|
564
528
|
|
|
529
|
+
The parent must have a resolved height. ReactFlow measures its container; inside
|
|
530
|
+
a `h-auto` wrapper it collapses to zero and renders nothing.
|
|
531
|
+
|
|
565
532
|
## Real-time Updates
|
|
566
533
|
|
|
534
|
+
Do not poll and do not hand-roll an event emitter. A resolver mutation anywhere —
|
|
535
|
+
UI, chat, MCP, CLI — broadcasts one SSE frame that every mounted surface on that
|
|
536
|
+
dimension receives:
|
|
537
|
+
|
|
567
538
|
```tsx
|
|
568
|
-
|
|
569
|
-
import {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
runtime.off('envelope:sent', updateFlow);
|
|
593
|
-
runtime.off('envelope:resolved', updateFlow);
|
|
594
|
-
runtime.off('agent:statusChange', updateFlow);
|
|
595
|
-
};
|
|
596
|
-
}, [runtime, setNodes, setEdges]);
|
|
539
|
+
import { useCallback } from 'react';
|
|
540
|
+
import { useNodesState, useEdgesState, ReactFlow, type Node, type Edge } from '@xyflow/react';
|
|
541
|
+
import { useSurfaceRefresh } from '@/lib/use-surface-refresh';
|
|
542
|
+
import { buildGraph, type WorkflowDef } from './build-graph';
|
|
543
|
+
|
|
544
|
+
export function WorkflowCanvas({ slug, workflowId }: { slug: string; workflowId: string }) {
|
|
545
|
+
const [nodes, setNodes] = useNodesState<Node>([]);
|
|
546
|
+
const [edges, setEdges] = useEdgesState<Edge>([]);
|
|
547
|
+
|
|
548
|
+
const reload = useCallback(() => {
|
|
549
|
+
fetch(`/api/workflows/${workflowId}?slug=${slug}`)
|
|
550
|
+
.then(r => r.json() as Promise<{ data?: WorkflowDef }>)
|
|
551
|
+
.then(d => {
|
|
552
|
+
if (!d.data) return;
|
|
553
|
+
const g = buildGraph(d.data);
|
|
554
|
+
setNodes(g.nodes);
|
|
555
|
+
setEdges(g.edges);
|
|
556
|
+
})
|
|
557
|
+
.catch(() => {});
|
|
558
|
+
}, [slug, workflowId, setNodes, setEdges]);
|
|
559
|
+
|
|
560
|
+
useSurfaceRefresh(slug, 'workflows', reload);
|
|
561
|
+
|
|
562
|
+
return <ReactFlow nodes={nodes} edges={edges} /* ... */ />;
|
|
597
563
|
}
|
|
598
564
|
```
|
|
599
565
|
|
|
600
566
|
## Best Practices
|
|
601
567
|
|
|
602
|
-
1. **
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
568
|
+
1. **Registries at module scope**: `NODE_TYPES` / `EDGE_TYPES` defined in the
|
|
569
|
+
component body remount every node on every render.
|
|
570
|
+
2. **`Omit<NodeProps,'data'> & { data: T }`**: the v12 pattern. `NodeProps<T>`
|
|
571
|
+
does not compile with a bare data interface.
|
|
572
|
+
3. **Index-signature your data types**: `[key: string]: unknown`, or the type is
|
|
573
|
+
rejected at `Node['data']`.
|
|
574
|
+
4. **Tokens inside the canvas too**: `var(--color-primary)`, `color-mix(...)`,
|
|
575
|
+
`var(--color-border)`. Never a hex, never a palette class.
|
|
576
|
+
5. **`client:only="react"`, sized parent**: the canvas cannot SSR and collapses
|
|
577
|
+
without a resolved height.
|
|
578
|
+
6. **Derive the graph in a pure module**: no React, no xyflow runtime import — so
|
|
579
|
+
it can be unit-tested.
|
|
580
|
+
7. **Default undefined edge `data`**: a newly-connected edge has none.
|
|
581
|
+
8. **`emitClick` before the handler**: node and edge clicks are signals.
|
|
582
|
+
9. **`proOptions={{ hideAttribution: true }}`**: on every canvas in the repo.
|
|
608
583
|
|
|
609
584
|
---
|
|
610
585
|
|
|
611
|
-
**
|
|
612
|
-
**
|
|
586
|
+
**Tech**: @xyflow/react 12.10.2 · @dagrejs/dagre 3.0 · React 19.2.6 · Astro 6.3.7
|
|
587
|
+
**Tree**: `one.ie/web` — 25 importing files under `src/components/`
|