@tuongaz/seeflow 0.1.64 → 0.1.65
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/dist/web/assets/{index-BAEA18IR.js → index-DL6aaddE.js} +985 -985
- package/dist/web/assets/{index.es-DZEdTXNJ.js → index.es-CVm3MRo3.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DT1Li8zz.js → jspdf.es.min-C06OvDJX.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/flow.json +28 -30
- package/examples/order-pipeline/flow.json +16 -16
- package/package.json +1 -1
- package/src/api.ts +19 -23
- package/src/cli-e2e.ts +6 -7
- package/src/cli-manifest.ts +3 -5
- package/src/cli.ts +26 -0
- package/src/diagram.ts +8 -5
- package/src/layout.ts +22 -26
- package/src/mcp.ts +2 -2
- package/src/merge.ts +4 -3
- package/src/node-files.ts +1 -1
- package/src/operations.ts +68 -50
- package/src/runtime.ts +37 -0
- package/src/schema-catalog.ts +23 -11
- package/src/schema.ts +147 -271
- package/src/server.ts +1 -1
- package/src/status-runner.ts +2 -1
- package/src/watcher.ts +7 -7
|
@@ -4,23 +4,22 @@
|
|
|
4
4
|
"nodes": [
|
|
5
5
|
{
|
|
6
6
|
"id": "node-cQOUPXanaX",
|
|
7
|
-
"type": "
|
|
7
|
+
"type": "user",
|
|
8
8
|
"data": {
|
|
9
|
-
"shape": "user",
|
|
10
9
|
"name": "Customer",
|
|
11
10
|
"description": "Web / Mobile / Partner"
|
|
12
11
|
}
|
|
13
12
|
},
|
|
14
13
|
{
|
|
15
14
|
"id": "node-CbwYqb7NfB",
|
|
16
|
-
"type": "
|
|
15
|
+
"type": "rectangle",
|
|
17
16
|
"data": {
|
|
18
17
|
"name": "API Gateway",
|
|
18
|
+
"description": "Auth, rate-limiting, routing.",
|
|
19
|
+
"detail": "file://detail.md",
|
|
19
20
|
"stateSource": {
|
|
20
21
|
"kind": "request"
|
|
21
22
|
},
|
|
22
|
-
"description": "Auth, rate-limiting, routing.",
|
|
23
|
-
"detail": "file://detail.md",
|
|
24
23
|
"playAction": {
|
|
25
24
|
"kind": "script",
|
|
26
25
|
"interpreter": "bun",
|
|
@@ -31,38 +30,38 @@
|
|
|
31
30
|
},
|
|
32
31
|
{
|
|
33
32
|
"id": "node-3zFtHg6ENc",
|
|
34
|
-
"type": "
|
|
33
|
+
"type": "rectangle",
|
|
35
34
|
"data": {
|
|
36
35
|
"name": "Auth Service",
|
|
36
|
+
"description": "JWT issuance + OAuth2 / OIDC.",
|
|
37
|
+
"detail": "file://detail.md",
|
|
37
38
|
"stateSource": {
|
|
38
39
|
"kind": "request"
|
|
39
|
-
}
|
|
40
|
-
"description": "JWT issuance + OAuth2 / OIDC.",
|
|
41
|
-
"detail": "file://detail.md"
|
|
40
|
+
}
|
|
42
41
|
}
|
|
43
42
|
},
|
|
44
43
|
{
|
|
45
44
|
"id": "node-kwBY8YPmYM",
|
|
46
|
-
"type": "
|
|
45
|
+
"type": "rectangle",
|
|
47
46
|
"data": {
|
|
48
47
|
"name": "Product Catalog",
|
|
48
|
+
"description": "SKUs, variants, pricing, full-text search.",
|
|
49
|
+
"detail": "file://detail.md",
|
|
49
50
|
"stateSource": {
|
|
50
51
|
"kind": "request"
|
|
51
|
-
}
|
|
52
|
-
"description": "SKUs, variants, pricing, full-text search.",
|
|
53
|
-
"detail": "file://detail.md"
|
|
52
|
+
}
|
|
54
53
|
}
|
|
55
54
|
},
|
|
56
55
|
{
|
|
57
56
|
"id": "node-5F424NWbEu",
|
|
58
|
-
"type": "
|
|
57
|
+
"type": "rectangle",
|
|
59
58
|
"data": {
|
|
60
59
|
"name": "Cart Service",
|
|
60
|
+
"description": "Add/remove items, apply coupons, checkout.",
|
|
61
|
+
"detail": "file://detail.md",
|
|
61
62
|
"stateSource": {
|
|
62
63
|
"kind": "request"
|
|
63
64
|
},
|
|
64
|
-
"description": "Add/remove items, apply coupons, checkout.",
|
|
65
|
-
"detail": "file://detail.md",
|
|
66
65
|
"playAction": {
|
|
67
66
|
"kind": "script",
|
|
68
67
|
"interpreter": "bun",
|
|
@@ -73,14 +72,14 @@
|
|
|
73
72
|
},
|
|
74
73
|
{
|
|
75
74
|
"id": "node-yKrg9DV5fJ",
|
|
76
|
-
"type": "
|
|
75
|
+
"type": "rectangle",
|
|
77
76
|
"data": {
|
|
78
77
|
"name": "Order Service",
|
|
78
|
+
"description": "pending → confirmed → shipped → delivered.",
|
|
79
|
+
"detail": "file://detail.md",
|
|
79
80
|
"stateSource": {
|
|
80
81
|
"kind": "event"
|
|
81
82
|
},
|
|
82
|
-
"description": "pending → confirmed → shipped → delivered.",
|
|
83
|
-
"detail": "file://detail.md",
|
|
84
83
|
"playAction": {
|
|
85
84
|
"kind": "script",
|
|
86
85
|
"interpreter": "bun",
|
|
@@ -91,40 +90,39 @@
|
|
|
91
90
|
},
|
|
92
91
|
{
|
|
93
92
|
"id": "node-mPqan8rFYN",
|
|
94
|
-
"type": "
|
|
93
|
+
"type": "rectangle",
|
|
95
94
|
"data": {
|
|
96
95
|
"name": "Payment Service",
|
|
96
|
+
"description": "Stripe + PayPal gateway integration.",
|
|
97
|
+
"detail": "file://detail.md",
|
|
97
98
|
"stateSource": {
|
|
98
99
|
"kind": "event"
|
|
99
|
-
}
|
|
100
|
-
"description": "Stripe + PayPal gateway integration.",
|
|
101
|
-
"detail": "file://detail.md"
|
|
100
|
+
}
|
|
102
101
|
}
|
|
103
102
|
},
|
|
104
103
|
{
|
|
105
104
|
"id": "node-fkptXw7uvs",
|
|
106
|
-
"type": "
|
|
105
|
+
"type": "rectangle",
|
|
107
106
|
"data": {
|
|
108
107
|
"name": "Notification Service",
|
|
108
|
+
"description": "Email + SMS + push via AWS SES / SNS.",
|
|
109
|
+
"detail": "file://detail.md",
|
|
109
110
|
"stateSource": {
|
|
110
111
|
"kind": "event"
|
|
111
|
-
}
|
|
112
|
-
"description": "Email + SMS + push via AWS SES / SNS.",
|
|
113
|
-
"detail": "file://detail.md"
|
|
112
|
+
}
|
|
114
113
|
}
|
|
115
114
|
},
|
|
116
115
|
{
|
|
117
116
|
"id": "node-5SDiw3Wz6s",
|
|
118
|
-
"type": "
|
|
117
|
+
"type": "database",
|
|
119
118
|
"data": {
|
|
120
|
-
"shape": "database",
|
|
121
119
|
"name": "PostgreSQL",
|
|
122
120
|
"description": "Orders, users, products — primary OLTP store"
|
|
123
121
|
}
|
|
124
122
|
},
|
|
125
123
|
{
|
|
126
124
|
"id": "node-XwygzfKPZ5",
|
|
127
|
-
"type": "
|
|
125
|
+
"type": "html",
|
|
128
126
|
"data": {
|
|
129
127
|
"name": "Platform Health",
|
|
130
128
|
"html": "file://view.html"
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"nodes": [
|
|
5
5
|
{
|
|
6
6
|
"id": "node-XKIyds0TDg",
|
|
7
|
-
"type": "
|
|
7
|
+
"type": "rectangle",
|
|
8
8
|
"data": {
|
|
9
9
|
"name": "POST /orders",
|
|
10
|
+
"description": "Creates order, kicks off the pipeline.",
|
|
11
|
+
"detail": "file://detail.md",
|
|
10
12
|
"stateSource": {
|
|
11
13
|
"kind": "request"
|
|
12
14
|
},
|
|
13
|
-
"description": "Creates order, kicks off the pipeline.",
|
|
14
|
-
"detail": "file://detail.md",
|
|
15
15
|
"playAction": {
|
|
16
16
|
"kind": "script",
|
|
17
17
|
"interpreter": "bun",
|
|
@@ -22,39 +22,39 @@
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"id": "node-GXTKUcE3ye",
|
|
25
|
-
"type": "
|
|
25
|
+
"type": "rectangle",
|
|
26
26
|
"data": {
|
|
27
27
|
"name": "Inventory Service",
|
|
28
|
-
"stateSource": {
|
|
29
|
-
"kind": "event"
|
|
30
|
-
},
|
|
31
28
|
"description": "Reserves stock.",
|
|
32
29
|
"detail": "file://detail.md",
|
|
33
|
-
"icon": "a-arrow-down-icon"
|
|
30
|
+
"icon": "a-arrow-down-icon",
|
|
31
|
+
"stateSource": {
|
|
32
|
+
"kind": "event"
|
|
33
|
+
}
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"id": "node-YOYiHJpY0i",
|
|
38
|
-
"type": "
|
|
38
|
+
"type": "rectangle",
|
|
39
39
|
"data": {
|
|
40
40
|
"name": "Payment Service",
|
|
41
|
+
"description": "Charges card.",
|
|
42
|
+
"detail": "file://detail.md",
|
|
41
43
|
"stateSource": {
|
|
42
44
|
"kind": "event"
|
|
43
|
-
}
|
|
44
|
-
"description": "Charges card.",
|
|
45
|
-
"detail": "file://detail.md"
|
|
45
|
+
}
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"id": "node-zUIH7WFnhK",
|
|
50
|
-
"type": "
|
|
50
|
+
"type": "rectangle",
|
|
51
51
|
"data": {
|
|
52
52
|
"name": "Fulfillment Service",
|
|
53
|
+
"description": "Enqueues shipment.",
|
|
54
|
+
"detail": "file://detail.md",
|
|
53
55
|
"stateSource": {
|
|
54
56
|
"kind": "event"
|
|
55
|
-
}
|
|
56
|
-
"description": "Enqueues shipment.",
|
|
57
|
-
"detail": "file://detail.md"
|
|
57
|
+
}
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
],
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -345,14 +345,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
345
345
|
}
|
|
346
346
|
const flow = flowParse.data;
|
|
347
347
|
const result = await computeLayout(
|
|
348
|
-
flow.nodes.map((n) => ({
|
|
349
|
-
id: n.id,
|
|
350
|
-
type: n.type,
|
|
351
|
-
// Only `shape` matters for layout (floating-annotation detection +
|
|
352
|
-
// shape-specific sizing). Other Flow data fields are irrelevant.
|
|
353
|
-
data:
|
|
354
|
-
n.type === 'shapeNode' ? { shape: (n.data as { shape?: string }).shape } : undefined,
|
|
355
|
-
})),
|
|
348
|
+
flow.nodes.map((n) => ({ id: n.id, type: n.type })),
|
|
356
349
|
flow.connectors.map((c) => ({ id: c.id, source: c.source, target: c.target })),
|
|
357
350
|
options,
|
|
358
351
|
);
|
|
@@ -375,12 +368,18 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
375
368
|
.map((n) => ({
|
|
376
369
|
id: n.id,
|
|
377
370
|
type: n.type as
|
|
378
|
-
| '
|
|
379
|
-
| '
|
|
380
|
-
| '
|
|
381
|
-
| '
|
|
382
|
-
| '
|
|
383
|
-
| '
|
|
371
|
+
| 'rectangle'
|
|
372
|
+
| 'ellipse'
|
|
373
|
+
| 'sticky'
|
|
374
|
+
| 'text'
|
|
375
|
+
| 'database'
|
|
376
|
+
| 'server'
|
|
377
|
+
| 'user'
|
|
378
|
+
| 'queue'
|
|
379
|
+
| 'cloud'
|
|
380
|
+
| 'image'
|
|
381
|
+
| 'html'
|
|
382
|
+
| 'icon',
|
|
384
383
|
data:
|
|
385
384
|
typeof n.width === 'number' && typeof n.height === 'number'
|
|
386
385
|
? { width: n.width, height: n.height }
|
|
@@ -582,7 +581,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
582
581
|
});
|
|
583
582
|
|
|
584
583
|
// POST /api/projects/:id/files/open — shell out to `$EDITOR <abs>` so the
|
|
585
|
-
// user can edit a project-scoped file (
|
|
584
|
+
// user can edit a project-scoped file (type:'html' block, image asset) in
|
|
586
585
|
// their IDE. The endpoint always returns the resolved absolute path in
|
|
587
586
|
// the response body so the frontend can copy-to-clipboard when $EDITOR
|
|
588
587
|
// isn't set or the spawn fails. Path safety mirrors the GET route.
|
|
@@ -815,13 +814,10 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
815
814
|
|
|
816
815
|
const node = merged.flow.nodes.find((n) => n.id === nodeId);
|
|
817
816
|
if (!node) return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
node.type === 'htmlNode' ||
|
|
823
|
-
!node.data.playAction
|
|
824
|
-
) {
|
|
817
|
+
// playAction is optional on every node type post-flat-types. The runtime
|
|
818
|
+
// gate is purely "is the field set?" — visual kind doesn't constrain
|
|
819
|
+
// playability.
|
|
820
|
+
if (!node.data.playAction) {
|
|
825
821
|
return c.json({ error: `Node ${nodeId} has no playAction` }, 400);
|
|
826
822
|
}
|
|
827
823
|
|
|
@@ -1007,7 +1003,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
1007
1003
|
});
|
|
1008
1004
|
|
|
1009
1005
|
// PATCH a single node — partial update of position, label, detail, visual
|
|
1010
|
-
// fields, or
|
|
1006
|
+
// fields, or geometric-only fields. Every UI-driven node edit (other than
|
|
1011
1007
|
// the high-frequency drag fast-path above) flows through here. The mutation
|
|
1012
1008
|
// is performed against the raw parsed JSON (so unknown v2 fields the schema
|
|
1013
1009
|
// doesn't yet recognize survive round-trips) and the WHOLE resulting demo
|
package/src/cli-e2e.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// End-to-end validator used by `seeflow e2e <flowId>`. Opens an SSE channel to
|
|
2
|
-
// the studio, triggers every
|
|
2
|
+
// the studio, triggers every node carrying a playAction (sequentially), then drains
|
|
3
3
|
// the channel for node:done/error + node:status reports. Mirrors the algorithm
|
|
4
4
|
// in skills/seeflow/scripts/validate-end-to-end.ts — kept here so the CLI does
|
|
5
5
|
// not depend on the skill folder.
|
|
@@ -167,15 +167,14 @@ function openSseChannel(body: ReadableStream<Uint8Array>): SseChannel {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function hasPlayAction(node: NodeShape): boolean {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// Flat-types refactor: capabilities are top-level data fields on every
|
|
171
|
+
// type, not gated by the type tag. The e2e runner iterates every node
|
|
172
|
+
// carrying a playAction regardless of variant.
|
|
173
|
+
return node.data?.playAction !== undefined;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
function hasStatusAction(node: NodeShape): boolean {
|
|
176
|
-
return
|
|
177
|
-
(node.type === 'playNode' || node.type === 'stateNode') && node.data?.statusAction !== undefined
|
|
178
|
-
);
|
|
177
|
+
return node.data?.statusAction !== undefined;
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
export async function validateEndToEnd(options: ValidateOptions): Promise<ValidationReport> {
|
package/src/cli-manifest.ts
CHANGED
|
@@ -281,7 +281,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
281
281
|
},
|
|
282
282
|
requiresStudio: false,
|
|
283
283
|
examples: [
|
|
284
|
-
'seeflow flow:add-bulk abc12345 --json \'{"nodes":[{"id":"a","type":"
|
|
284
|
+
'seeflow flow:add-bulk abc12345 --json \'{"nodes":[{"id":"a","type":"rectangle","data":{}}],"connectors":[]}\'',
|
|
285
285
|
'seeflow flow:add-bulk abc12345 --file batch.json',
|
|
286
286
|
],
|
|
287
287
|
},
|
|
@@ -344,7 +344,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
344
344
|
flags: BODY_FLAGS,
|
|
345
345
|
body: {
|
|
346
346
|
example: {
|
|
347
|
-
type: '
|
|
347
|
+
type: 'rectangle',
|
|
348
348
|
data: { name: 'hello', stateSource: { kind: 'request' } },
|
|
349
349
|
},
|
|
350
350
|
},
|
|
@@ -360,9 +360,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
360
360
|
],
|
|
361
361
|
},
|
|
362
362
|
requiresStudio: false,
|
|
363
|
-
examples: [
|
|
364
|
-
'seeflow nodes:add abc12345 --json \'{"type":"shapeNode","data":{"shape":"rectangle"}}\'',
|
|
365
|
-
],
|
|
363
|
+
examples: ['seeflow nodes:add abc12345 --json \'{"type":"rectangle","data":{}}\''],
|
|
366
364
|
},
|
|
367
365
|
{
|
|
368
366
|
name: 'nodes:get',
|
package/src/cli.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
clearPid,
|
|
21
21
|
defaultPidPath,
|
|
22
22
|
isPidAlive,
|
|
23
|
+
portInUse,
|
|
23
24
|
readConfig,
|
|
24
25
|
readPid,
|
|
25
26
|
studioUrl,
|
|
@@ -266,6 +267,20 @@ async function runHelp() {
|
|
|
266
267
|
console.log(renderCommandList());
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// Pre-flight: refuse to start if the studio port already has a TCP listener.
|
|
271
|
+
// We deliberately do NOT probe Vite's port (5173) here — `bun run dev` spawns
|
|
272
|
+
// Vite alongside the studio, so Vite legitimately owns 5173 in dev mode and a
|
|
273
|
+
// probe can't distinguish "our Vite" from a stranger. If Vite's port is taken
|
|
274
|
+
// by something else, Vite itself surfaces the conflict.
|
|
275
|
+
async function assertPortFree(studioPort: number, host: string): Promise<void> {
|
|
276
|
+
if (!(await portInUse(host, studioPort))) return;
|
|
277
|
+
console.error(
|
|
278
|
+
`Cannot start SeeFlow: port ${studioPort} already in use.\n` +
|
|
279
|
+
`Stop the running server on ${studioPort} first, then retry.`,
|
|
280
|
+
);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
269
284
|
async function runStart() {
|
|
270
285
|
mkdirSync(seeflowHome(), { recursive: true });
|
|
271
286
|
const config = readConfig();
|
|
@@ -294,6 +309,12 @@ async function runStart() {
|
|
|
294
309
|
return;
|
|
295
310
|
}
|
|
296
311
|
|
|
312
|
+
// Defense-in-depth: parent already checked in spawnDaemon, but a race
|
|
313
|
+
// between parent-check and child-bind can still let another process grab
|
|
314
|
+
// the port. Re-check here so the child fails fast with a clear error
|
|
315
|
+
// instead of EADDRINUSE at bind time.
|
|
316
|
+
await assertPortFree(port, config.host);
|
|
317
|
+
|
|
297
318
|
// persist the chosen address so other subcommands can find us
|
|
298
319
|
writeConfig({ port, host: config.host });
|
|
299
320
|
|
|
@@ -368,6 +389,11 @@ async function spawnDaemon(port: number, host: string) {
|
|
|
368
389
|
return;
|
|
369
390
|
}
|
|
370
391
|
|
|
392
|
+
// Studio port must be free before we fork a detached child — otherwise the
|
|
393
|
+
// user waits HEALTH_TIMEOUT_MS for a doomed health probe before seeing a
|
|
394
|
+
// generic timeout error.
|
|
395
|
+
await assertPortFree(port, host);
|
|
396
|
+
|
|
371
397
|
const proc = spawnDetachedStudio(port);
|
|
372
398
|
writePid(proc.pid);
|
|
373
399
|
writeConfig({ port, host });
|
package/src/diagram.ts
CHANGED
|
@@ -240,9 +240,10 @@ const normalizeConnectors = (
|
|
|
240
240
|
// Single-flow-node graphs short-circuit so callers can pin standalone
|
|
241
241
|
// nodes via `layout.positions`.
|
|
242
242
|
const isFloatingAnnotation = (n: Record<string, unknown>): boolean => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
// Flat-types refactor: visual kind IS the type. The annotation shapes
|
|
244
|
+
// (sticky + text) live as top-level type tags rather than nested shape
|
|
245
|
+
// under a generic shape variant.
|
|
246
|
+
return n.type === 'sticky' || n.type === 'text';
|
|
246
247
|
};
|
|
247
248
|
|
|
248
249
|
const autoLayout = async (
|
|
@@ -354,12 +355,14 @@ export const validateDemo = (req: ValidateRequest): ValidateReport => {
|
|
|
354
355
|
|
|
355
356
|
const playable = nodes.filter((n) => {
|
|
356
357
|
const data = n.data as { playAction?: unknown } | undefined;
|
|
357
|
-
|
|
358
|
+
// Flat-types refactor: a node is playable iff it carries a playAction
|
|
359
|
+
// capability, regardless of variant.
|
|
360
|
+
return data?.playAction !== undefined;
|
|
358
361
|
});
|
|
359
362
|
if (tier !== 'static' && playable.length === 0) {
|
|
360
363
|
issues.push({
|
|
361
364
|
kind: 'tier-mismatch',
|
|
362
|
-
message: `Tier '${tier}' requires at least one playable node; found 0.
|
|
365
|
+
message: `Tier '${tier}' requires at least one playable node; found 0. Set data.playAction on a node or set tier=static.`,
|
|
363
366
|
});
|
|
364
367
|
}
|
|
365
368
|
|
package/src/layout.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface LayoutResult {
|
|
|
51
51
|
export interface LayoutNode {
|
|
52
52
|
id: string;
|
|
53
53
|
type: FlowNode['type'];
|
|
54
|
-
data?: { width?: number; height?: number
|
|
54
|
+
data?: { width?: number; height?: number };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export interface LayoutEdge {
|
|
@@ -60,42 +60,38 @@ export interface LayoutEdge {
|
|
|
60
60
|
target: string;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Per-type default size used when a node has no explicit width/height. Mirrors
|
|
64
|
+
// the canvas's SHAPE_DEFAULT_SIZE so ELK's layout matches what the canvas will
|
|
65
|
+
// paint.
|
|
63
66
|
const DEFAULT_DIMENSIONS: Record<FlowNode['type'], { width: number; height: number }> = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
iconNode: { width: 80, height: 80 },
|
|
68
|
-
htmlNode: { width: 320, height: 200 },
|
|
69
|
-
imageNode: { width: 200, height: 150 },
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const SHAPE_OVERRIDES: Record<string, { width: number; height: number }> = {
|
|
67
|
+
rectangle: { width: 200, height: 120 },
|
|
68
|
+
ellipse: { width: 200, height: 120 },
|
|
69
|
+
sticky: { width: 180, height: 180 },
|
|
73
70
|
text: { width: 160, height: 40 },
|
|
74
|
-
|
|
71
|
+
database: { width: 120, height: 140 },
|
|
72
|
+
server: { width: 140, height: 120 },
|
|
73
|
+
user: { width: 100, height: 140 },
|
|
74
|
+
queue: { width: 220, height: 80 },
|
|
75
|
+
cloud: { width: 180, height: 120 },
|
|
76
|
+
image: { width: 200, height: 150 },
|
|
77
|
+
html: { width: 320, height: 200 },
|
|
78
|
+
icon: { width: 80, height: 80 },
|
|
75
79
|
};
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
// Sticky / text variants are floating annotations. They never participate in
|
|
82
|
+
// layered layout — they sit in a side column so the orthogonal flow stays
|
|
83
|
+
// clean.
|
|
84
|
+
const FLOATING_TYPES: ReadonlySet<FlowNode['type']> = new Set(['sticky', 'text']);
|
|
78
85
|
|
|
79
86
|
const nodeDimensions = (node: LayoutNode): { width: number; height: number } => {
|
|
80
87
|
const data = node.data ?? {};
|
|
81
88
|
if (typeof data.width === 'number' && typeof data.height === 'number') {
|
|
82
89
|
return { width: data.width, height: data.height };
|
|
83
90
|
}
|
|
84
|
-
if (node.type === 'shapeNode' && data.shape) {
|
|
85
|
-
const override = SHAPE_OVERRIDES[data.shape];
|
|
86
|
-
if (override) return override;
|
|
87
|
-
}
|
|
88
91
|
return DEFAULT_DIMENSIONS[node.type];
|
|
89
92
|
};
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
// layered layout — they sit in a side column so the orthogonal flow stays
|
|
93
|
-
// clean.
|
|
94
|
-
const isFloatingAnnotation = (node: LayoutNode): boolean => {
|
|
95
|
-
if (node.type !== 'shapeNode') return false;
|
|
96
|
-
const shape = node.data?.shape;
|
|
97
|
-
return shape !== undefined && FLOATING_SHAPES.has(shape);
|
|
98
|
-
};
|
|
94
|
+
const isFloatingAnnotation = (node: LayoutNode): boolean => FLOATING_TYPES.has(node.type);
|
|
99
95
|
|
|
100
96
|
// Schema vocabulary: SourceHandle ∈ {r, b}, TargetHandle ∈ {t, l}. After
|
|
101
97
|
// ELK lays out positions we pick handles geometrically — the layered LR
|
|
@@ -162,7 +158,7 @@ export const computeLayout = async (
|
|
|
162
158
|
'elk.separateConnectedComponents': 'true',
|
|
163
159
|
},
|
|
164
160
|
children: laidOut.map((n) => {
|
|
165
|
-
const d = dims.get(n.id) ?? DEFAULT_DIMENSIONS.
|
|
161
|
+
const d = dims.get(n.id) ?? DEFAULT_DIMENSIONS.rectangle;
|
|
166
162
|
return { id: n.id, width: d.width, height: d.height };
|
|
167
163
|
}),
|
|
168
164
|
edges: connectors
|
|
@@ -193,7 +189,7 @@ export const computeLayout = async (
|
|
|
193
189
|
const columnX = laidOut.length > 0 ? maxRight + 200 : 0;
|
|
194
190
|
let cursorY = 0;
|
|
195
191
|
for (const n of floatingNodes) {
|
|
196
|
-
const d = dims.get(n.id) ?? DEFAULT_DIMENSIONS.
|
|
192
|
+
const d = dims.get(n.id) ?? DEFAULT_DIMENSIONS.rectangle;
|
|
197
193
|
result.nodes[n.id] = { position: { x: columnX, y: cursorY } };
|
|
198
194
|
cursorY += d.height + nodeSpacing;
|
|
199
195
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -461,7 +461,7 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
461
461
|
{
|
|
462
462
|
name: 'seeflow_add_node',
|
|
463
463
|
description:
|
|
464
|
-
|
|
464
|
+
"Append a new node to a flow (cascade-safe; id auto-generated when omitted). Text content fields (detail on every node; html on type:'html') are auto-externalized to <project>/nodes/<id>/ and stored as file:// refs in flow.json; reads inline the resolved content transparently.",
|
|
465
465
|
inputSchema: inputSchemaFromZod(AddNodeInputSchema),
|
|
466
466
|
handler: async (args) => {
|
|
467
467
|
const parsed = AddNodeInputSchema.safeParse(args);
|
|
@@ -585,7 +585,7 @@ const buildTools = (ops: Operations): McpTool[] => [
|
|
|
585
585
|
{
|
|
586
586
|
name: 'seeflow_patch_node',
|
|
587
587
|
description:
|
|
588
|
-
|
|
588
|
+
"Update fields on an existing node (position, name, description, detail, icon, colors, border, font, dimensions, autoSize, plus type:'icon'-only color/strokeWidth/alt and capabilities playAction/statusAction/stateSource). `type` can flip a node between any of the 12 visual variants (rectangle/ellipse/sticky/text/database/server/user/queue/cloud/image/html/icon); the post-merge schema reparse gates required fields on the new type (image.data.path, icon.data.icon). Setting detail (every node) or html (type:'html') writes the content to <project>/nodes/<id>/{detail.md|view.html}; the file:// ref on the node persists. Empty-string detail empties the file but keeps the ref.",
|
|
589
589
|
inputSchema: inputSchemaFromZod(PatchNodeInputSchema),
|
|
590
590
|
handler: async (args) => {
|
|
591
591
|
const parsed = PatchNodeInputSchema.safeParse(args);
|
package/src/merge.ts
CHANGED
|
@@ -39,10 +39,12 @@ export function mergeFlowAndStyle(flow: Flow, style: Style): ResolvedFlow {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Fields that live in a node's `data` block on flow.json. Every other data
|
|
42
|
-
// field is visual and routes to style.json.
|
|
42
|
+
// field is visual and routes to style.json. The flat-types refactor folds
|
|
43
|
+
// the visual kind into `node.type` itself (no more nested data.shape / data.kind)
|
|
44
|
+
// and makes every capability (playAction / statusAction / stateSource) valid
|
|
45
|
+
// on every type.
|
|
43
46
|
const NODE_DATA_FLOW_KEYS = new Set([
|
|
44
47
|
'name',
|
|
45
|
-
'kind',
|
|
46
48
|
'stateSource',
|
|
47
49
|
'handlerModule',
|
|
48
50
|
'icon',
|
|
@@ -50,7 +52,6 @@ const NODE_DATA_FLOW_KEYS = new Set([
|
|
|
50
52
|
'detail',
|
|
51
53
|
'playAction',
|
|
52
54
|
'statusAction',
|
|
53
|
-
'shape',
|
|
54
55
|
'path',
|
|
55
56
|
'alt',
|
|
56
57
|
'html',
|
package/src/node-files.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface ExternalizedFieldSpec {
|
|
|
14
14
|
|
|
15
15
|
export const EXTERNALIZED_NODE_FIELDS: readonly ExternalizedFieldSpec[] = [
|
|
16
16
|
{ field: 'detail', fileName: 'detail.md' },
|
|
17
|
-
{ field: 'html', fileName: 'view.html', nodeTypes: ['
|
|
17
|
+
{ field: 'html', fileName: 'view.html', nodeTypes: ['html'] },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
export const externalizedFieldsForNodeType = (
|