pi-agent-fleet 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -23
- package/assets/canvas-node-peek.png +0 -0
- package/assets/canvas.png +0 -0
- package/package.json +11 -3
- package/src/canvas-client.tsx +669 -0
- package/src/canvas.ts +570 -319
- package/src/controller.ts +8 -2
- package/src/dag.ts +58 -0
- package/src/scheduler.ts +113 -0
- package/src/worktree.ts +75 -0
package/src/canvas.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
import type { AddressInfo } from "node:net";
|
|
5
|
-
import { basename, join } from "node:path";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { promisify } from "node:util";
|
|
7
9
|
import type { ActiveFleet } from "./controller.js";
|
|
8
10
|
import { readState } from "./state.js";
|
|
@@ -36,6 +38,7 @@ export interface CanvasPayload {
|
|
|
36
38
|
lgtm_streak: number;
|
|
37
39
|
paused: boolean;
|
|
38
40
|
cost_usd_estimate: number;
|
|
41
|
+
demo?: boolean;
|
|
39
42
|
loop?: { gate: string; max_iterations: number; lgtm_count: number };
|
|
40
43
|
config: { max_concurrent: number; model?: string; effort?: string; warn_cost_usd?: number };
|
|
41
44
|
nodes: CanvasNodeView[];
|
|
@@ -95,6 +98,382 @@ export function buildCanvasPayload(fleet: ActiveFleet): CanvasPayload {
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/** Baked snapshot of a real fleet (quickcall-zero-to-hero) used as the demo /
|
|
102
|
+
fallback view when no fleet is live. Structure is hardcoded, not read from disk. */
|
|
103
|
+
const DEMO_FLEET = (
|
|
104
|
+
{
|
|
105
|
+
"fleet_name": "quickcall-zero-to-hero",
|
|
106
|
+
"status": "running",
|
|
107
|
+
"created_at": "2026-08-02T12:49:37.320Z",
|
|
108
|
+
"iteration": 1,
|
|
109
|
+
"lgtm_streak": 0,
|
|
110
|
+
"paused": false,
|
|
111
|
+
"cost_usd_estimate": 13.8376735,
|
|
112
|
+
"loop": {
|
|
113
|
+
"gate": "reviewer",
|
|
114
|
+
"max_iterations": 2,
|
|
115
|
+
"lgtm_count": 1
|
|
116
|
+
},
|
|
117
|
+
"config": {
|
|
118
|
+
"max_concurrent": 4,
|
|
119
|
+
"model": "gpt-5.4",
|
|
120
|
+
"warn_cost_usd": 50
|
|
121
|
+
},
|
|
122
|
+
"nodes": [
|
|
123
|
+
{
|
|
124
|
+
"id": "l1-methods",
|
|
125
|
+
"type": "research",
|
|
126
|
+
"task": "You are L1 (lay-of-the-land) researcher in a 2-layer research fleet. Mission context: founder built QuickCall — a daemon watching engineers' AI coding-agent sessions (Claude Code, Cursor, Codex), extracting team conventions, capturing ac…",
|
|
127
|
+
"status": "completed",
|
|
128
|
+
"model": "gpt-5.4",
|
|
129
|
+
"turns": 84,
|
|
130
|
+
"tokens": 3446330,
|
|
131
|
+
"cost_usd_estimate": 1.7322095000000008,
|
|
132
|
+
"produced_outputs": [
|
|
133
|
+
"output/l1-methods.md"
|
|
134
|
+
],
|
|
135
|
+
"outputs": [
|
|
136
|
+
{
|
|
137
|
+
"path": "output/l1-methods.md",
|
|
138
|
+
"kind": "markdown",
|
|
139
|
+
"required": true
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"depends_on": [],
|
|
143
|
+
"iterate": true,
|
|
144
|
+
"worktree": false
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "l1-models-data",
|
|
148
|
+
"type": "research",
|
|
149
|
+
"task": "You are L1 (lay-of-the-land) researcher in a 2-layer fleet. Mission context: founder built QuickCall — daemon watching engineers' AI coding-agent sessions, capturing accept/reject signals and human corrections on agent output. Pitch in 2…",
|
|
150
|
+
"status": "completed",
|
|
151
|
+
"model": "gpt-5.4",
|
|
152
|
+
"turns": 30,
|
|
153
|
+
"tokens": 3795144,
|
|
154
|
+
"cost_usd_estimate": 2.4356045,
|
|
155
|
+
"produced_outputs": [
|
|
156
|
+
"output/l1-models-data.md"
|
|
157
|
+
],
|
|
158
|
+
"outputs": [
|
|
159
|
+
{
|
|
160
|
+
"path": "output/l1-models-data.md",
|
|
161
|
+
"kind": "markdown",
|
|
162
|
+
"required": true
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"depends_on": [],
|
|
166
|
+
"iterate": true,
|
|
167
|
+
"worktree": false
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "l1-economics",
|
|
171
|
+
"type": "research",
|
|
172
|
+
"task": "You are L1 (lay-of-the-land) researcher in a 2-layer fleet. Context: founder pitching Head of AI at a foundation lab in 2 days — post-training open code models on preference traces from QuickCall (daemon capturing accept/reject/correctio…",
|
|
173
|
+
"status": "completed",
|
|
174
|
+
"model": "gpt-5.4",
|
|
175
|
+
"turns": 77,
|
|
176
|
+
"tokens": 2852546,
|
|
177
|
+
"cost_usd_estimate": 1.447467,
|
|
178
|
+
"produced_outputs": [
|
|
179
|
+
"output/l1-economics.md"
|
|
180
|
+
],
|
|
181
|
+
"outputs": [
|
|
182
|
+
{
|
|
183
|
+
"path": "output/l1-economics.md",
|
|
184
|
+
"kind": "markdown",
|
|
185
|
+
"required": true
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
"depends_on": [],
|
|
189
|
+
"iterate": true,
|
|
190
|
+
"worktree": false
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "l1-market",
|
|
194
|
+
"type": "research",
|
|
195
|
+
"task": "You are L1 (lay-of-the-land) researcher in a 2-layer fleet. Context: founder pitching Head of AI at a foundation lab in 2 days. Startup QuickCall: daemon on dev machines capturing AI coding-agent sessions → team conventions + accept/reje…",
|
|
196
|
+
"status": "completed",
|
|
197
|
+
"model": "gpt-5.4",
|
|
198
|
+
"turns": 28,
|
|
199
|
+
"tokens": 1473836,
|
|
200
|
+
"cost_usd_estimate": 1.2037,
|
|
201
|
+
"produced_outputs": [
|
|
202
|
+
"output/l1-market.md"
|
|
203
|
+
],
|
|
204
|
+
"outputs": [
|
|
205
|
+
{
|
|
206
|
+
"path": "output/l1-market.md",
|
|
207
|
+
"kind": "markdown",
|
|
208
|
+
"required": true
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
"depends_on": [],
|
|
212
|
+
"iterate": true,
|
|
213
|
+
"worktree": false
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "l2-deep-methods",
|
|
217
|
+
"type": "research",
|
|
218
|
+
"task": "You are L2 (double-down) researcher in a 2-layer fleet. Context: founder pitching Head of AI at a foundation lab in 2 days — post-training open code models on preference traces from QuickCall (daemon capturing accept/reject/corrections f…",
|
|
219
|
+
"status": "completed",
|
|
220
|
+
"model": "gpt-5.4",
|
|
221
|
+
"turns": 88,
|
|
222
|
+
"tokens": 5299748,
|
|
223
|
+
"cost_usd_estimate": 1.9066499999999997,
|
|
224
|
+
"produced_outputs": [
|
|
225
|
+
"output/l2-deep-methods.md"
|
|
226
|
+
],
|
|
227
|
+
"outputs": [
|
|
228
|
+
{
|
|
229
|
+
"path": "output/l2-deep-methods.md",
|
|
230
|
+
"kind": "markdown",
|
|
231
|
+
"required": true
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"depends_on": [
|
|
235
|
+
"l1-methods",
|
|
236
|
+
"l1-models-data",
|
|
237
|
+
"l1-economics",
|
|
238
|
+
"l1-market"
|
|
239
|
+
],
|
|
240
|
+
"iterate": true,
|
|
241
|
+
"worktree": false
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"id": "l2-deep-data",
|
|
245
|
+
"type": "research",
|
|
246
|
+
"task": "You are L2 (double-down) researcher in a 2-layer fleet. Context: founder pitching Head of AI at a foundation lab in 2 days — post-training open code models on preference traces from QuickCall. QuickCall daemon captures: agent suggestions…",
|
|
247
|
+
"status": "completed",
|
|
248
|
+
"model": "gpt-5.4",
|
|
249
|
+
"turns": 50,
|
|
250
|
+
"tokens": 5052879,
|
|
251
|
+
"cost_usd_estimate": 2.2376524999999994,
|
|
252
|
+
"produced_outputs": [
|
|
253
|
+
"output/l2-deep-data.md"
|
|
254
|
+
],
|
|
255
|
+
"outputs": [
|
|
256
|
+
{
|
|
257
|
+
"path": "output/l2-deep-data.md",
|
|
258
|
+
"kind": "markdown",
|
|
259
|
+
"required": true
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"depends_on": [
|
|
263
|
+
"l1-methods",
|
|
264
|
+
"l1-models-data",
|
|
265
|
+
"l1-economics",
|
|
266
|
+
"l1-market"
|
|
267
|
+
],
|
|
268
|
+
"iterate": true,
|
|
269
|
+
"worktree": false
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"id": "l2-deep-pilot",
|
|
273
|
+
"type": "research",
|
|
274
|
+
"task": "You are L2 (double-down) researcher in a 2-layer fleet. Context: founder pitching Head of AI at a foundation lab in 2 days — post-training open code models on preference traces from QuickCall. L1 surveys at workers/l1-*/output/l1-*.md — …",
|
|
275
|
+
"status": "completed",
|
|
276
|
+
"model": "gpt-5.4",
|
|
277
|
+
"turns": 43,
|
|
278
|
+
"tokens": 3580622,
|
|
279
|
+
"cost_usd_estimate": 2.1080365,
|
|
280
|
+
"produced_outputs": [
|
|
281
|
+
"output/l2-deep-pilot.md"
|
|
282
|
+
],
|
|
283
|
+
"outputs": [
|
|
284
|
+
{
|
|
285
|
+
"path": "output/l2-deep-pilot.md",
|
|
286
|
+
"kind": "markdown",
|
|
287
|
+
"required": true
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
"depends_on": [
|
|
291
|
+
"l1-methods",
|
|
292
|
+
"l1-models-data",
|
|
293
|
+
"l1-economics",
|
|
294
|
+
"l1-market"
|
|
295
|
+
],
|
|
296
|
+
"iterate": true,
|
|
297
|
+
"worktree": false
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"id": "l2-deep-defense",
|
|
301
|
+
"type": "research",
|
|
302
|
+
"task": "You are L2 (double-down) researcher in a 2-layer fleet. Context: founder pitching Head of AI at a foundation lab in 2 days — post-training open code models on preference traces from QuickCall (daemon capturing accept/reject/corrections i…",
|
|
303
|
+
"status": "completed",
|
|
304
|
+
"model": "gpt-5.4",
|
|
305
|
+
"turns": 19,
|
|
306
|
+
"tokens": 776077,
|
|
307
|
+
"cost_usd_estimate": 0.7663535,
|
|
308
|
+
"produced_outputs": [
|
|
309
|
+
"output/l2-deep-defense.md"
|
|
310
|
+
],
|
|
311
|
+
"outputs": [
|
|
312
|
+
{
|
|
313
|
+
"path": "output/l2-deep-defense.md",
|
|
314
|
+
"kind": "markdown",
|
|
315
|
+
"required": true
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
"depends_on": [
|
|
319
|
+
"l1-methods",
|
|
320
|
+
"l1-models-data",
|
|
321
|
+
"l1-economics",
|
|
322
|
+
"l1-market"
|
|
323
|
+
],
|
|
324
|
+
"iterate": true,
|
|
325
|
+
"worktree": false
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"id": "reading-pack",
|
|
329
|
+
"type": "write",
|
|
330
|
+
"task": "You are the fan-in synthesis node of a 2-layer research fleet. Read all eight research files in the fleet workspace: workers/l1-*/output/l1-*.md and workers/l2-*/output/l2-*.md. Context: founder of QuickCall (daemon capturing preference …",
|
|
331
|
+
"status": "running",
|
|
332
|
+
"model": "gpt-5.4",
|
|
333
|
+
"turns": 0,
|
|
334
|
+
"tokens": 0,
|
|
335
|
+
"cost_usd_estimate": 0,
|
|
336
|
+
"produced_outputs": [],
|
|
337
|
+
"outputs": [
|
|
338
|
+
{
|
|
339
|
+
"path": "output/zero-to-hero.md",
|
|
340
|
+
"kind": "markdown",
|
|
341
|
+
"required": true
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"path": "output/talk-track.md",
|
|
345
|
+
"kind": "markdown",
|
|
346
|
+
"required": true
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
"depends_on": [
|
|
350
|
+
"l2-deep-methods",
|
|
351
|
+
"l2-deep-data",
|
|
352
|
+
"l2-deep-pilot",
|
|
353
|
+
"l2-deep-defense"
|
|
354
|
+
],
|
|
355
|
+
"iterate": true,
|
|
356
|
+
"worktree": false
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"id": "gap-reviewer",
|
|
360
|
+
"type": "reviewer",
|
|
361
|
+
"task": "You are the review gate of a 2-layer research fleet. Deliverable under review: output/zero-to-hero.md and output/talk-track.md (find in fleet workspace), synthesizing L1 surveys (workers/l1-*/output/) and L2 deep-dives (workers/l2-*/outp…",
|
|
362
|
+
"status": "pending",
|
|
363
|
+
"model": "k3",
|
|
364
|
+
"turns": 0,
|
|
365
|
+
"tokens": 0,
|
|
366
|
+
"cost_usd_estimate": 0,
|
|
367
|
+
"produced_outputs": [],
|
|
368
|
+
"outputs": [
|
|
369
|
+
{
|
|
370
|
+
"path": "output/verdict.md",
|
|
371
|
+
"kind": "verdict",
|
|
372
|
+
"required": true
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
"depends_on": [
|
|
376
|
+
"reading-pack"
|
|
377
|
+
],
|
|
378
|
+
"iterate": true,
|
|
379
|
+
"worktree": false
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
"edges": [
|
|
383
|
+
{
|
|
384
|
+
"from": "l1-methods",
|
|
385
|
+
"to": "l2-deep-methods"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"from": "l1-models-data",
|
|
389
|
+
"to": "l2-deep-methods"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"from": "l1-economics",
|
|
393
|
+
"to": "l2-deep-methods"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"from": "l1-market",
|
|
397
|
+
"to": "l2-deep-methods"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"from": "l1-methods",
|
|
401
|
+
"to": "l2-deep-data"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"from": "l1-models-data",
|
|
405
|
+
"to": "l2-deep-data"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"from": "l1-economics",
|
|
409
|
+
"to": "l2-deep-data"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"from": "l1-market",
|
|
413
|
+
"to": "l2-deep-data"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
"from": "l1-methods",
|
|
417
|
+
"to": "l2-deep-pilot"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"from": "l1-models-data",
|
|
421
|
+
"to": "l2-deep-pilot"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"from": "l1-economics",
|
|
425
|
+
"to": "l2-deep-pilot"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"from": "l1-market",
|
|
429
|
+
"to": "l2-deep-pilot"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"from": "l1-methods",
|
|
433
|
+
"to": "l2-deep-defense"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"from": "l1-models-data",
|
|
437
|
+
"to": "l2-deep-defense"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
"from": "l1-economics",
|
|
441
|
+
"to": "l2-deep-defense"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"from": "l1-market",
|
|
445
|
+
"to": "l2-deep-defense"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"from": "l2-deep-methods",
|
|
449
|
+
"to": "reading-pack"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"from": "l2-deep-data",
|
|
453
|
+
"to": "reading-pack"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"from": "l2-deep-pilot",
|
|
457
|
+
"to": "reading-pack"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
"from": "l2-deep-defense",
|
|
461
|
+
"to": "reading-pack"
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"from": "reading-pack",
|
|
465
|
+
"to": "gap-reviewer"
|
|
466
|
+
}
|
|
467
|
+
],
|
|
468
|
+
"iterations": [],
|
|
469
|
+
"demo": true
|
|
470
|
+
}
|
|
471
|
+
) as Omit<CanvasPayload, "generated_at">;
|
|
472
|
+
|
|
473
|
+
export function buildDemoPayload(): CanvasPayload {
|
|
474
|
+
return { ...DEMO_FLEET, generated_at: new Date().toISOString() };
|
|
475
|
+
}
|
|
476
|
+
|
|
98
477
|
export interface SessionEntryView {
|
|
99
478
|
role: string;
|
|
100
479
|
text: string;
|
|
@@ -134,335 +513,202 @@ async function latestSessionFile(workerDir: string): Promise<string | undefined>
|
|
|
134
513
|
}
|
|
135
514
|
}
|
|
136
515
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<html>
|
|
140
|
-
<head>
|
|
141
|
-
<meta charset="utf-8">
|
|
142
|
-
<title>fleet canvas</title>
|
|
143
|
-
<style>
|
|
144
|
-
:root { color-scheme: dark; }
|
|
145
|
-
body {
|
|
146
|
-
--bg:#0d1117; --fg:#c9d1d9; --muted:#8b949e; --line:#30363d; --panel:#161b22;
|
|
147
|
-
--accent:#58a6ff; --ok:#3fb950; --bad:#f85149; --warn:#d29922; --wire:#6e7681; --hdr:#f0f6fc;
|
|
148
|
-
}
|
|
149
|
-
body.light { color-scheme: light;
|
|
150
|
-
--bg:#f6f8fa; --fg:#1f2328; --muted:#57606a; --line:#d0d7de; --panel:#ffffff;
|
|
151
|
-
--accent:#0969da; --ok:#1a7f37; --bad:#cf222e; --warn:#9a6700; --wire:#8c959f; --hdr:#1f2328;
|
|
152
|
-
}
|
|
153
|
-
* { box-sizing:border-box; }
|
|
154
|
-
body { margin:0; font:13px/1.45 -apple-system, Menlo, monospace; background:var(--bg); color:var(--fg); }
|
|
155
|
-
header { padding:8px 14px; border-bottom:1px solid var(--line); display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
|
|
156
|
-
header .name { font-weight:700; color:var(--hdr); }
|
|
157
|
-
#hdr { display:flex; gap:12px; align-items:baseline; }
|
|
158
|
-
.pill { padding:1px 8px; border-radius:10px; border:1px solid var(--line); }
|
|
159
|
-
button, select { background:var(--panel); color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:3px 10px; font:inherit; cursor:pointer; }
|
|
160
|
-
button:hover, select:hover { border-color:var(--accent); }
|
|
161
|
-
main { display:flex; height:calc(100vh - 42px); }
|
|
162
|
-
#stage { flex:1; position:relative; overflow:hidden; }
|
|
163
|
-
#viewport { position:absolute; left:0; top:0; transform-origin:0 0; }
|
|
164
|
-
.node { position:absolute; width:260px; border:1px solid var(--line); border-radius:8px; padding:8px 10px; cursor:pointer; background:var(--panel); }
|
|
165
|
-
.node:hover { border-color:var(--accent); }
|
|
166
|
-
.node.sel { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
|
|
167
|
-
.node .nid { font-weight:700; color:var(--hdr); }
|
|
168
|
-
.badge { float:right; font-size:10px; padding:0 6px; border:1px solid var(--line); border-radius:8px; color:var(--muted); }
|
|
169
|
-
.meta { color:var(--muted); font-size:12px; }
|
|
170
|
-
.stats { font-size:12px; margin-top:2px; }
|
|
171
|
-
.note { color:var(--warn); font-size:12px; margin-top:4px; }
|
|
172
|
-
.out-chip { display:inline-block; font-size:10px; border:1px solid var(--line); border-radius:8px; padding:0 5px; margin:3px 3px 0 0; color:var(--muted); }
|
|
173
|
-
.flags { font-size:10px; color:var(--warn); margin-top:3px; }
|
|
174
|
-
.st-completed { border-left:4px solid var(--ok); }
|
|
175
|
-
.st-running { border-left:4px solid var(--accent); }
|
|
176
|
-
.st-failed, .st-contract_failed { border-left:4px solid var(--bad); }
|
|
177
|
-
.st-killed, .st-blocked { border-left:4px solid var(--muted); }
|
|
178
|
-
.st-pending, .st-ready { border-left:4px solid var(--line); }
|
|
179
|
-
#side { width:430px; border-left:1px solid var(--line); overflow:auto; padding:12px; display:none; background:var(--bg); }
|
|
180
|
-
#side.open { display:block; }
|
|
181
|
-
.msg { margin-bottom:10px; padding:8px; border-radius:6px; background:var(--panel); white-space:pre-wrap; word-break:break-word; }
|
|
182
|
-
.msg .role { font-weight:700; margin-bottom:4px; }
|
|
183
|
-
.role-user { color:var(--accent); } .role-assistant { color:var(--ok); } .role-tool { color:var(--warn); }
|
|
184
|
-
.taskbox { border:1px solid var(--line); border-radius:6px; padding:8px; margin-bottom:10px; white-space:pre-wrap; word-break:break-word; font-size:12px; }
|
|
185
|
-
#empty { padding:40px; color:var(--muted); }
|
|
186
|
-
</style>
|
|
187
|
-
</head>
|
|
188
|
-
<body>
|
|
189
|
-
<header>
|
|
190
|
-
<span class="name">fleet canvas</span>
|
|
191
|
-
<select id="fleetSel"></select>
|
|
192
|
-
<span id="hdr"></span>
|
|
193
|
-
<span style="flex:1"></span>
|
|
194
|
-
<button onclick="fitView()">fit</button>
|
|
195
|
-
<button onclick="resetView()">1:1</button>
|
|
196
|
-
<button id="themeBtn" onclick="toggleTheme()">theme</button>
|
|
197
|
-
</header>
|
|
198
|
-
<main>
|
|
199
|
-
<div id="stage"></div>
|
|
200
|
-
<div id="side"></div>
|
|
201
|
-
</main>
|
|
202
|
-
<script>
|
|
203
|
-
var MIN_ZOOM = 0.2, MAX_ZOOM = 2.5;
|
|
204
|
-
var camera = { x: 40, y: 40, scale: 1 };
|
|
205
|
-
var selected = null;
|
|
206
|
-
var currentFleet = null;
|
|
207
|
-
var stage = document.getElementById("stage");
|
|
208
|
-
var viewport = document.createElement("div");
|
|
209
|
-
viewport.id = "viewport";
|
|
210
|
-
stage.appendChild(viewport);
|
|
516
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
517
|
+
const requireFrom = createRequire(import.meta.url);
|
|
211
518
|
|
|
212
|
-
|
|
213
|
-
function esc(s){ return String(s).replace(/[&<>"]/g, function(c){ return { "&":"&", "<":"<", ">":">", '"':""" }[c]; }); }
|
|
214
|
-
|
|
215
|
-
/* theme */
|
|
216
|
-
function applyTheme(t){
|
|
217
|
-
document.body.className = t === "light" ? "light" : "";
|
|
218
|
-
try { localStorage.setItem("fleet-canvas-theme", t); } catch(e) {}
|
|
219
|
-
}
|
|
220
|
-
function toggleTheme(){
|
|
221
|
-
applyTheme(document.body.className === "light" ? "dark" : "light");
|
|
222
|
-
}
|
|
223
|
-
(function(){
|
|
224
|
-
var qsTheme = new URLSearchParams(location.search).get("theme");
|
|
225
|
-
var t = qsTheme === "light" || qsTheme === "dark" ? qsTheme : null;
|
|
226
|
-
try { if(!t) t = localStorage.getItem("fleet-canvas-theme"); } catch(e) {}
|
|
227
|
-
if(!t && window.matchMedia && matchMedia("(prefers-color-scheme: light)").matches) t = "light";
|
|
228
|
-
applyTheme(t || "dark");
|
|
229
|
-
})();
|
|
519
|
+
let bundleCache: Promise<string> | undefined;
|
|
230
520
|
|
|
231
|
-
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
var r = stage.getBoundingClientRect();
|
|
246
|
-
zoomAt(e.clientX - r.left, e.clientY - r.top, Math.exp(-e.deltaY * 0.0015));
|
|
247
|
-
}, { passive: false });
|
|
248
|
-
var dragging = false, lastX = 0, lastY = 0;
|
|
249
|
-
stage.addEventListener("pointerdown", function(e){
|
|
250
|
-
if(e.target.closest && e.target.closest(".node")) return;
|
|
251
|
-
dragging = true; lastX = e.clientX; lastY = e.clientY;
|
|
252
|
-
stage.setPointerCapture(e.pointerId);
|
|
253
|
-
});
|
|
254
|
-
stage.addEventListener("pointermove", function(e){
|
|
255
|
-
if(!dragging) return;
|
|
256
|
-
camera.x += e.clientX - lastX;
|
|
257
|
-
camera.y += e.clientY - lastY;
|
|
258
|
-
lastX = e.clientX; lastY = e.clientY;
|
|
259
|
-
applyCamera();
|
|
260
|
-
});
|
|
261
|
-
stage.addEventListener("pointerup", function(){ dragging = false; });
|
|
262
|
-
function fitView(){
|
|
263
|
-
var els = viewport.querySelectorAll(".node");
|
|
264
|
-
if(!els.length) return;
|
|
265
|
-
var minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
|
|
266
|
-
els.forEach(function(el){
|
|
267
|
-
var x = el.offsetLeft, y = el.offsetTop;
|
|
268
|
-
minX = Math.min(minX, x); minY = Math.min(minY, y);
|
|
269
|
-
maxX = Math.max(maxX, x + el.offsetWidth); maxY = Math.max(maxY, y + el.offsetHeight);
|
|
521
|
+
/** Bundle the React canvas client with esbuild (cached after first build). */
|
|
522
|
+
async function buildClientBundle(): Promise<string> {
|
|
523
|
+
const esbuild = await import("esbuild");
|
|
524
|
+
const result = await esbuild.build({
|
|
525
|
+
entryPoints: [join(HERE, "canvas-client.tsx")],
|
|
526
|
+
bundle: true,
|
|
527
|
+
format: "iife",
|
|
528
|
+
platform: "browser",
|
|
529
|
+
target: "es2020",
|
|
530
|
+
jsx: "automatic",
|
|
531
|
+
minify: true,
|
|
532
|
+
write: false,
|
|
533
|
+
logLevel: "silent",
|
|
534
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
270
535
|
});
|
|
271
|
-
|
|
272
|
-
var z = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min(sw / (maxX - minX), sh / (maxY - minY))));
|
|
273
|
-
camera.scale = z;
|
|
274
|
-
camera.x = 30 - minX * z + (sw - (maxX - minX) * z) / 2;
|
|
275
|
-
camera.y = 30 - minY * z + (sh - (maxY - minY) * z) / 2;
|
|
276
|
-
applyCamera();
|
|
536
|
+
return result.outputFiles[0].text;
|
|
277
537
|
}
|
|
278
|
-
function resetView(){ camera = { x: 40, y: 40, scale: 1 }; applyCamera(); }
|
|
279
538
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
edges.forEach(function(e){ if(e.from in indeg){ indeg[e.to]++; rev[e.from].push(e.to); } });
|
|
286
|
-
var out=[]; var cur=ids.filter(function(i){ return indeg[i]===0; }); var seen={};
|
|
287
|
-
while(cur.length){
|
|
288
|
-
out.push(cur);
|
|
289
|
-
cur.forEach(function(i){ seen[i]=true; });
|
|
290
|
-
var next=[];
|
|
291
|
-
cur.forEach(function(i){ rev[i].forEach(function(m){ indeg[m]--; if(indeg[m]===0) next.push(m); }); });
|
|
292
|
-
cur=next.filter(function(i){ return !seen[i]; });
|
|
539
|
+
function flowCss(): string {
|
|
540
|
+
try {
|
|
541
|
+
return readFileSyncSafe(requireFrom.resolve("@xyflow/react/dist/style.css"));
|
|
542
|
+
} catch {
|
|
543
|
+
return "";
|
|
293
544
|
}
|
|
294
|
-
ids.forEach(function(i){ if(!seen[i]) out.push([i]); });
|
|
295
|
-
return out;
|
|
296
|
-
}
|
|
297
|
-
function layout(s){
|
|
298
|
-
var L = layers(s.nodes, s.edges);
|
|
299
|
-
var pos = {};
|
|
300
|
-
L.forEach(function(layer, li){
|
|
301
|
-
layer.forEach(function(id, ni){
|
|
302
|
-
pos[id] = { x: li * 340, y: ni * 160 };
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
return pos;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/* render */
|
|
309
|
-
function cardHtml(n){
|
|
310
|
-
var chips = (n.outputs || []).map(function(o){
|
|
311
|
-
return '<span class="out-chip">' + esc(o.path) + " · " + esc(o.kind) + '</span>';
|
|
312
|
-
}).join("");
|
|
313
|
-
var flags = [];
|
|
314
|
-
if(n.iterate === false) flags.push("once");
|
|
315
|
-
if(n.worktree) flags.push("worktree");
|
|
316
|
-
return '<div class="nid">' + esc(n.id) + '<span class="badge">' + esc(n.type) + '</span></div>'
|
|
317
|
-
+ '<div class="meta">' + esc(n.status) + ' · ' + esc(n.model) + (n.effort ? " · " + esc(n.effort) : "") + '</div>'
|
|
318
|
-
+ '<div class="stats">' + String(n.turns|0) + ' turns · ' + (Number(n.tokens||0)/1000).toFixed(1) + 'k tok · $' + Number(n.cost_usd_estimate||0).toFixed(2) + '</div>'
|
|
319
|
-
+ (chips ? '<div>' + chips + '</div>' : "")
|
|
320
|
-
+ (flags.length ? '<div class="flags">' + flags.join(" · ") + '</div>' : "")
|
|
321
|
-
+ (n.status_note ? '<div class="note">' + esc(n.status_note) + '</div>' : "");
|
|
322
545
|
}
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
var byId = {};
|
|
327
|
-
s.nodes.forEach(function(n){
|
|
328
|
-
var el = document.createElement("div");
|
|
329
|
-
el.className = "node st-" + n.status + (selected === n.id ? " sel" : "");
|
|
330
|
-
el.setAttribute("data-id", n.id);
|
|
331
|
-
el.style.left = pos[n.id].x + "px";
|
|
332
|
-
el.style.top = pos[n.id].y + "px";
|
|
333
|
-
el.innerHTML = cardHtml(n);
|
|
334
|
-
el.onclick = function(){ sel(n.id); };
|
|
335
|
-
viewport.appendChild(el);
|
|
336
|
-
n._lx = pos[n.id].x; n._ly = pos[n.id].y; n._el = el;
|
|
337
|
-
byId[n.id] = n;
|
|
338
|
-
});
|
|
339
|
-
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
340
|
-
svg.style.position = "absolute";
|
|
341
|
-
svg.style.left = "0"; svg.style.top = "0";
|
|
342
|
-
svg.style.pointerEvents = "none";
|
|
343
|
-
svg.style.overflow = "visible";
|
|
344
|
-
svg.setAttribute("width", "1"); svg.setAttribute("height", "1");
|
|
345
|
-
s.edges.forEach(function(e){
|
|
346
|
-
var a = byId[e.from], b = byId[e.to];
|
|
347
|
-
if(!a || !b) return;
|
|
348
|
-
var x1 = a._lx + 260;
|
|
349
|
-
var y1 = a._ly + (a._el ? a._el.offsetHeight : 110) / 2;
|
|
350
|
-
var x2 = b._lx;
|
|
351
|
-
var y2 = b._ly + (b._el ? b._el.offsetHeight : 110) / 2;
|
|
352
|
-
var mx = (x1 + x2) / 2;
|
|
353
|
-
var p = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
354
|
-
p.setAttribute("d", "M " + x1 + " " + y1 + " C " + mx + " " + y1 + ", " + mx + " " + y2 + ", " + x2 + " " + y2);
|
|
355
|
-
p.setAttribute("fill", "none");
|
|
356
|
-
p.setAttribute("stroke", "var(--wire)");
|
|
357
|
-
p.setAttribute("stroke-width", "2");
|
|
358
|
-
svg.appendChild(p);
|
|
359
|
-
var ah = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
360
|
-
ah.setAttribute("d", "M " + (x2-7) + " " + (y2-4.5) + " L " + x2 + " " + y2 + " L " + (x2-7) + " " + (y2+4.5));
|
|
361
|
-
ah.setAttribute("fill", "none");
|
|
362
|
-
ah.setAttribute("stroke", "var(--wire)");
|
|
363
|
-
ah.setAttribute("stroke-width", "2");
|
|
364
|
-
svg.appendChild(ah);
|
|
365
|
-
});
|
|
366
|
-
viewport.appendChild(svg);
|
|
546
|
+
function readFileSyncSafe(p: string): string {
|
|
547
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
548
|
+
return (requireFrom("node:fs") as typeof import("node:fs")).readFileSync(p, "utf-8");
|
|
367
549
|
}
|
|
368
550
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
551
|
+
const PAGE_CSS = `
|
|
552
|
+
:root { color-scheme: dark; --bg:#0d1117; --fg:#c9d1d9; --muted:#8b949e; --line:#30363d; --panel:#1a2029; --panel-2:#212936; --stage-bg:#0a0c10; --accent:#58a6ff; --ok:#3fb950; --bad:#f85149; --warn:#d29922; --wire:#6e7681; --hdr:#f0f6fc; --card-shadow:0 1px 2px rgba(0,0,0,0.5), 0 10px 24px -8px rgba(0,0,0,0.65); --card-shadow-lg:0 2px 4px rgba(0,0,0,0.5), 0 16px 36px -10px rgba(0,0,0,0.75); --edge:#4a5361; --edge-soft:#363d49; --mm-mask:rgba(8,10,14,0.55); --mm-frame:rgba(255,255,255,0.14); }
|
|
553
|
+
[data-theme="light"] { color-scheme: light; --bg:#f6f8fa; --fg:#1f2328; --muted:#57606a; --line:#d0d7de; --panel:#ffffff; --panel-2:#f6f8fa; --stage-bg:#eef1f5; --accent:#0969da; --ok:#1a7f37; --bad:#cf222e; --warn:#9a6700; --wire:#8c959f; --hdr:#1f2328; --card-shadow:0 1px 2px rgba(15,23,42,0.08), 0 10px 24px -8px rgba(15,23,42,0.18); --card-shadow-lg:0 2px 4px rgba(15,23,42,0.10), 0 16px 36px -10px rgba(15,23,42,0.24); --edge:#b5bdc9; --edge-soft:#d0d7de; --mm-mask:rgba(15,23,42,0.10); --mm-frame:rgba(9,105,218,0.35); }
|
|
554
|
+
:root { --ui:system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --mono:"SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; }
|
|
555
|
+
* { box-sizing:border-box; }
|
|
556
|
+
html,body,#root { margin:0; height:100%; }
|
|
557
|
+
html,body { overflow:hidden; font:13px/1.45 var(--ui); background:var(--bg); color:var(--fg); }
|
|
558
|
+
/* monospace is reserved for identifiers, code paths, measurements, and transcripts */
|
|
559
|
+
.id, .stats, .out-chip, .badge, .fp-name, .fp-status, .fp-trigger-status, .taskbox-side, .msg, .empty code { font-family:var(--mono); }
|
|
560
|
+
#root { display:flex; flex-direction:column; }
|
|
561
|
+
header { padding:8px 14px; border-bottom:1px solid var(--line); display:flex; gap:12px; align-items:center; flex-wrap:wrap; background:var(--panel); flex:0 0 auto; }
|
|
562
|
+
header .name { font-weight:700; color:var(--hdr); }
|
|
563
|
+
#hdr { display:flex; flex-wrap:wrap; gap:6px 12px; align-items:center; min-width:0; }
|
|
564
|
+
.pill { padding:1px 8px; border-radius:10px; border:1px solid var(--line); }
|
|
565
|
+
button, select { background:var(--panel); color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:5px 11px; min-height:30px; font:inherit; cursor:pointer; }
|
|
566
|
+
button:hover, select:hover { border-color:var(--accent); }
|
|
567
|
+
:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:6px; }
|
|
568
|
+
.icon-btn { display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; min-height:30px; padding:0; font-size:18px; line-height:1; color:var(--muted); }
|
|
569
|
+
.icon-btn:hover { color:var(--fg); }
|
|
570
|
+
.fp { position:relative; }
|
|
571
|
+
.fp-trigger { display:flex; align-items:center; gap:7px; min-width:210px; max-width:360px; padding:5px 11px; min-height:30px; text-align:left; }
|
|
572
|
+
.fp-trigger-status { font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:8px; padding:0 6px; white-space:nowrap; }
|
|
573
|
+
.fp-label { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
574
|
+
.fp-caret { color:var(--muted); font-size:11px; }
|
|
575
|
+
.dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; background:var(--muted); }
|
|
576
|
+
.dot.live { background:transparent; border:2px solid var(--accent); }
|
|
577
|
+
.fp-menu { position:absolute; top:calc(100% + 6px); left:0; width:340px; max-width:78vw; background:var(--panel); border-radius:12px; box-shadow:var(--card-shadow-lg); z-index:50; overflow:hidden; }
|
|
578
|
+
.fp-search { width:100%; border:none; border-bottom:1px solid var(--line); border-radius:0; padding:9px 12px; background:transparent; color:var(--fg); }
|
|
579
|
+
.fp-search:focus-visible { outline-offset:-2px; }
|
|
580
|
+
.fp-list { max-height:340px; overflow-y:auto; padding:4px 0; }
|
|
581
|
+
.fp-item { display:flex; align-items:center; gap:9px; padding:8px 12px; min-height:34px; cursor:pointer; }
|
|
582
|
+
.fp-item.active { background:color-mix(in srgb, var(--fg) 8%, transparent); }
|
|
583
|
+
.fp-item.selected .fp-name { color:var(--accent); font-weight:600; }
|
|
584
|
+
.fp-name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
585
|
+
.fp-status { font-size:11px; color:var(--muted); white-space:nowrap; flex-shrink:0; }
|
|
586
|
+
.fp-empty { padding:10px; color:var(--muted); text-align:center; }
|
|
587
|
+
.taskbox-side-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:4px; }
|
|
588
|
+
main { display:flex; flex:1 1 auto; min-height:0; }
|
|
589
|
+
#stage { flex:1; position:relative; min-width:0; background:var(--stage-bg); }
|
|
590
|
+
.react-flow { background:var(--stage-bg); }
|
|
591
|
+
.react-flow__node { width:284px; }
|
|
592
|
+
.react-flow__handle { width:6px; height:6px; background:var(--wire); border:none; opacity:0; }
|
|
593
|
+
.react-flow__edge-path { stroke:var(--edge); stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
|
|
594
|
+
.react-flow__edge:hover .react-flow__edge-path { stroke:var(--accent); stroke-width:2; }
|
|
595
|
+
.react-flow__edge.animated .react-flow__edge-path { stroke:var(--accent); stroke-width:2; stroke-dasharray:1 7; animation:dashflow 0.7s linear infinite; }
|
|
596
|
+
@keyframes dashflow { to { stroke-dashoffset:-16; } }
|
|
597
|
+
.react-flow__controls button { background:var(--panel); color:var(--fg); border-bottom:1px solid var(--line); fill:var(--fg); }
|
|
598
|
+
.react-flow__minimap { width:172px; height:112px; background:var(--panel); border-radius:12px; box-shadow:var(--card-shadow); overflow:hidden; }
|
|
599
|
+
.react-flow__minimap svg { border-radius:10px; }
|
|
600
|
+
.react-flow__minimap-mask { fill:var(--mm-mask) !important; stroke:none; }
|
|
601
|
+
.react-flow__minimap-node { stroke:var(--panel) !important; stroke-width:4px !important; rx:3; ry:3; }
|
|
602
|
+
.react-flow__attribution { display:none; }
|
|
603
|
+
.node { position:relative; width:284px; border-radius:12px; background:var(--panel); cursor:pointer; box-shadow:var(--card-shadow); transition:box-shadow 0.18s ease, transform 0.18s ease; }
|
|
604
|
+
.node:hover { box-shadow:var(--card-shadow-lg); transform:translateY(-2px); }
|
|
605
|
+
.node.sel { background:color-mix(in srgb, var(--accent) 14%, var(--panel)); box-shadow:var(--card-shadow-lg); transform:translateY(-2px); }
|
|
606
|
+
.node.sel.st-failed, .node.sel.st-contract_failed { background:color-mix(in srgb, var(--bad) 14%, var(--panel)); }
|
|
607
|
+
/* keyboard focus ring only — never on mouse click */
|
|
608
|
+
.node:focus:not(:focus-visible) { outline:none; }
|
|
609
|
+
.node.st-running { background:color-mix(in srgb, var(--accent) 10%, var(--panel)); }
|
|
610
|
+
.node.st-failed, .node.st-contract_failed { background:color-mix(in srgb, var(--bad) 11%, var(--panel)); }
|
|
611
|
+
.card-body { padding:13px 14px; }
|
|
612
|
+
.node-header { display:flex; align-items:center; gap:7px; }
|
|
613
|
+
.node-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
614
|
+
.node-dot.pulse { animation:pulse 1.6s ease-in-out infinite; }
|
|
615
|
+
.node-header .id { flex:1; }
|
|
616
|
+
.node-header .badge { margin-left:auto; }
|
|
617
|
+
.id { font-weight:700; font-size:16px; letter-spacing:-0.01em; color:var(--hdr); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
618
|
+
.badge { font-size:11px; padding:1px 7px; border:1px solid var(--line); border-radius:10px; color:var(--muted); white-space:nowrap; }
|
|
619
|
+
.status-row { margin-top:7px; display:flex; align-items:center; gap:6px; font-size:13px; color:var(--muted); }
|
|
620
|
+
.status-row .st-word { color:var(--fg); font-weight:600; }
|
|
621
|
+
.spinner { width:10px; height:10px; border:2px solid var(--accent); border-top-color:transparent; border-radius:50%; animation:spin 1s linear infinite; display:inline-block; flex-shrink:0; }
|
|
622
|
+
@keyframes spin { to { transform:rotate(360deg); } }
|
|
623
|
+
.stats { margin-top:4px; font-size:13px; color:var(--muted); }
|
|
624
|
+
.outputs { margin-top:6px; }
|
|
625
|
+
.out-chip { display:inline-block; font-size:11px; line-height:1.5; border:1px solid var(--line); border-radius:8px; padding:5px 8px; margin:4px 4px 0 0; color:var(--muted); background:var(--bg); }
|
|
626
|
+
.flags { margin-top:6px; font-size:11px; color:var(--warn); }
|
|
627
|
+
.flags span { cursor:help; }
|
|
628
|
+
.note { margin-top:6px; font-size:13px; color:var(--warn); }
|
|
629
|
+
#side { width:420px; flex:0 0 auto; border-left:1px solid var(--line); overflow:auto; padding:12px; background:var(--bg); }
|
|
630
|
+
#side:focus, #side:focus-visible { outline:none; }
|
|
631
|
+
.side-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
|
|
632
|
+
.side-head .meta { color:var(--muted); }
|
|
633
|
+
.taskbox-side { border:1px solid var(--line); border-radius:6px; padding:8px; margin-bottom:10px; white-space:pre-wrap; word-break:break-word; font-size:13px; }
|
|
634
|
+
.msg { margin-bottom:10px; padding:8px; border-radius:6px; background:var(--panel); white-space:pre-wrap; word-break:break-word; }
|
|
635
|
+
.msg .role { font-weight:700; margin-bottom:4px; }
|
|
636
|
+
.role-user { color:var(--accent); } .role-assistant { color:var(--ok); } .role-tool { color:var(--warn); }
|
|
637
|
+
.react-flow__minimap-node.st-completed { fill:var(--ok); }
|
|
638
|
+
.react-flow__minimap-node.st-running { fill:var(--accent); }
|
|
639
|
+
.react-flow__minimap-node.st-failed, .react-flow__minimap-node.st-contract_failed { fill:var(--bad); }
|
|
640
|
+
.react-flow__minimap-node.st-killed, .react-flow__minimap-node.st-blocked { fill:var(--wire); }
|
|
641
|
+
.react-flow__minimap-node.st-pending, .react-flow__minimap-node.st-ready { fill:var(--line); }
|
|
642
|
+
/* header status strip */
|
|
643
|
+
.spacer { flex:1; }
|
|
644
|
+
.fleet-title { font-weight:700; font-size:16px; color:var(--hdr); max-width:34ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
645
|
+
.stat { display:inline-flex; align-items:center; gap:5px; color:var(--muted); }
|
|
646
|
+
.pill.status-running { color:var(--accent); border-color:var(--accent); }
|
|
647
|
+
.pill.status-completed { color:var(--ok); border-color:var(--ok); }
|
|
648
|
+
.pill.status-failed, .pill.status-contract_failed { color:var(--bad); border-color:var(--bad); }
|
|
649
|
+
.pill-bad { color:var(--bad); border-color:var(--bad); }
|
|
650
|
+
.pill-btn { display:inline-flex; align-items:center; min-height:24px; cursor:pointer; font:inherit; padding:3px 9px; }
|
|
651
|
+
.pill-btn:hover { background:var(--bg); }
|
|
652
|
+
.dot-run { background:var(--accent); animation:pulse 1.6s ease-in-out infinite; }
|
|
653
|
+
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.35; } }
|
|
654
|
+
.conn { display:inline-flex; align-items:center; gap:8px; }
|
|
655
|
+
.link-btn { display:inline-flex; align-items:center; min-height:24px; background:none; border:none; padding:2px 8px; color:var(--accent); text-decoration:underline; cursor:pointer; }
|
|
656
|
+
button.toggled { border-color:var(--accent); color:var(--accent); }
|
|
657
|
+
/* legend */
|
|
658
|
+
.legend { position:absolute; left:12px; bottom:12px; z-index:20; min-width:190px; padding:10px 12px; background:var(--panel); border-radius:12px; box-shadow:var(--card-shadow-lg); font-size:13px; }
|
|
659
|
+
.legend-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:7px; font-size:11px; letter-spacing:0.06em; text-transform:uppercase; color:var(--muted); }
|
|
660
|
+
.legend-row { display:flex; align-items:center; gap:9px; padding:2px 0; }
|
|
661
|
+
.swatch { width:12px; height:12px; border-radius:3px; flex-shrink:0; background:var(--line); }
|
|
662
|
+
.swatch.st-completed { background:var(--ok); }
|
|
663
|
+
.swatch.st-running { background:var(--accent); }
|
|
664
|
+
.swatch.st-failed { background:var(--bad); }
|
|
665
|
+
.swatch.st-blocked { background:var(--wire); }
|
|
666
|
+
.swatch.st-pending { background:var(--line); }
|
|
667
|
+
.swatch-line { width:16px; height:0; border-top:2px dashed var(--warn); flex-shrink:0; }
|
|
668
|
+
.legend-loop { color:var(--warn); margin-top:2px; }
|
|
669
|
+
.icon-btn.sm { width:24px; height:24px; min-height:24px; font-size:16px; }
|
|
670
|
+
/* empty state */
|
|
671
|
+
.empty { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; text-align:center; padding:24px; }
|
|
672
|
+
.empty-title { font-size:19px; font-weight:700; color:var(--hdr); }
|
|
673
|
+
.empty-body { max-width:460px; margin:0; color:var(--muted); }
|
|
674
|
+
.empty-steps { max-width:460px; margin:0; text-align:left; color:var(--muted); line-height:1.8; padding-left:18px; }
|
|
675
|
+
.empty code { background:var(--panel); border:1px solid var(--line); border-radius:4px; padding:1px 5px; }
|
|
676
|
+
.empty-cta { border-color:var(--accent); color:var(--accent); padding:8px 16px; }
|
|
677
|
+
.empty-cta:hover { background:var(--panel); }
|
|
678
|
+
/* failure reason surfaced on failed cards */
|
|
679
|
+
.fail-reason { margin-top:6px; font-size:13px; color:var(--bad); }
|
|
680
|
+
/* respect reduced-motion: keep the state, drop the perpetual movement */
|
|
681
|
+
@media (prefers-reduced-motion: reduce) {
|
|
682
|
+
.spinner { animation:none; border-top-color:var(--accent); opacity:0.6; }
|
|
683
|
+
.dot-run, .node-dot.pulse { animation:none; }
|
|
684
|
+
.node:hover { transform:none; }
|
|
685
|
+
.react-flow__edge.animated .react-flow__edge-path { animation:none; }
|
|
686
|
+
* { scroll-behavior:auto; }
|
|
415
687
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
side();
|
|
688
|
+
/* narrow viewports: side panel overlays the stage, header controls stay reachable */
|
|
689
|
+
@media (max-width:700px) {
|
|
690
|
+
.spacer { display:none; }
|
|
691
|
+
#side { position:absolute; top:0; right:0; bottom:0; width:min(420px,100%); z-index:40; box-shadow:-10px 0 30px rgba(0,0,0,0.45); }
|
|
692
|
+
.fp-trigger { min-width:150px; }
|
|
693
|
+
.legend { bottom:auto; top:12px; }
|
|
423
694
|
}
|
|
695
|
+
`;
|
|
424
696
|
|
|
425
|
-
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
currentFleet = pick;
|
|
441
|
-
selEl.value = pick;
|
|
442
|
-
}
|
|
443
|
-
tick();
|
|
444
|
-
var qs = new URLSearchParams(location.search);
|
|
445
|
-
var pre = qs.get("node");
|
|
446
|
-
if(pre) sel(pre);
|
|
447
|
-
}).catch(function(){
|
|
448
|
-
tick();
|
|
449
|
-
var qs = new URLSearchParams(location.search);
|
|
450
|
-
var pre = qs.get("node");
|
|
451
|
-
if(pre) sel(pre);
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
document.getElementById("fleetSel").addEventListener("change", function(e){
|
|
455
|
-
currentFleet = e.target.value || null;
|
|
456
|
-
try { localStorage.setItem("fleet-canvas-fleet", currentFleet || ""); } catch(err) {}
|
|
457
|
-
selected = null;
|
|
458
|
-
document.getElementById("side").classList.remove("open");
|
|
459
|
-
tick();
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
loadFleets();
|
|
463
|
-
setInterval(tick, 1500);
|
|
464
|
-
setInterval(side, 2000);
|
|
465
|
-
</script>
|
|
697
|
+
/** Full canvas HTML page with the React/@xyflow bundle inlined. Cached after first build. */
|
|
698
|
+
export async function renderCanvasPage(): Promise<string> {
|
|
699
|
+
bundleCache ??= buildClientBundle();
|
|
700
|
+
const [bundle] = await Promise.all([bundleCache]);
|
|
701
|
+
return `<!doctype html>
|
|
702
|
+
<html>
|
|
703
|
+
<head>
|
|
704
|
+
<meta charset="utf-8">
|
|
705
|
+
<title>fleet canvas</title>
|
|
706
|
+
<style>${flowCss()}</style>
|
|
707
|
+
<style>${PAGE_CSS}</style>
|
|
708
|
+
</head>
|
|
709
|
+
<body>
|
|
710
|
+
<div id="root"></div>
|
|
711
|
+
<script>${bundle}</script>
|
|
466
712
|
</body>
|
|
467
713
|
</html>`;
|
|
468
714
|
}
|
|
@@ -540,7 +786,12 @@ export async function startCanvasServer(opts: {
|
|
|
540
786
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
541
787
|
if (url.pathname === "/") {
|
|
542
788
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
543
|
-
res.end(renderCanvasPage());
|
|
789
|
+
res.end(await renderCanvasPage());
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (url.pathname === "/api/demo") {
|
|
793
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
794
|
+
res.end(JSON.stringify(buildDemoPayload()));
|
|
544
795
|
return;
|
|
545
796
|
}
|
|
546
797
|
const resolveFleet = async (name: string | null): Promise<ActiveFleet | undefined | "unknown"> => {
|