pi-agent-fleet 0.3.0 → 0.5.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 +99 -23
- package/assets/canvas-node-peek.png +0 -0
- package/assets/canvas.png +0 -0
- package/examples/json-number-pipeline.json +35 -0
- package/package.json +11 -3
- package/src/canvas-client.tsx +820 -0
- package/src/canvas.ts +740 -401
- package/src/command.ts +56 -14
- package/src/contracts.ts +68 -16
- package/src/controller.ts +33 -6
- package/src/dag.ts +85 -2
- package/src/edits.ts +8 -9
- package/src/fleet-recovery.ts +73 -0
- package/src/fleet-store.ts +11 -2
- package/src/insert.ts +2 -8
- package/src/model-resolution.ts +27 -2
- package/src/preferences.ts +10 -1
- package/src/prompts.ts +10 -0
- package/src/report.ts +28 -1
- package/src/scheduler.ts +116 -2
- package/src/tools.ts +67 -14
- package/src/types.ts +8 -0
- package/src/worktree.ts +81 -0
package/src/canvas.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { readdir, readFile } 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
|
-
import {
|
|
9
|
-
|
|
10
|
+
import { listFleetRoots, readDiskFleet } from "./fleet-recovery.js";
|
|
11
|
+
|
|
12
|
+
export { listFleetRoots, readDiskFleet };
|
|
13
|
+
export type { FleetRootInfo } from "./fleet-recovery.js";
|
|
10
14
|
|
|
11
15
|
const execFileP = promisify(execFile);
|
|
12
16
|
|
|
@@ -36,6 +40,7 @@ export interface CanvasPayload {
|
|
|
36
40
|
lgtm_streak: number;
|
|
37
41
|
paused: boolean;
|
|
38
42
|
cost_usd_estimate: number;
|
|
43
|
+
demo?: boolean;
|
|
39
44
|
loop?: { gate: string; max_iterations: number; lgtm_count: number };
|
|
40
45
|
config: { max_concurrent: number; model?: string; effort?: string; warn_cost_usd?: number };
|
|
41
46
|
nodes: CanvasNodeView[];
|
|
@@ -95,36 +100,510 @@ export function buildCanvasPayload(fleet: ActiveFleet): CanvasPayload {
|
|
|
95
100
|
};
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
/** Baked snapshot of a real fleet (quickcall-zero-to-hero) used as the demo /
|
|
104
|
+
fallback view when no fleet is live. Structure is hardcoded, not read from disk. */
|
|
105
|
+
const DEMO_FLEET = (
|
|
106
|
+
{
|
|
107
|
+
"fleet_name": "quickcall-zero-to-hero",
|
|
108
|
+
"status": "running",
|
|
109
|
+
"created_at": "2026-08-02T12:49:37.320Z",
|
|
110
|
+
"iteration": 1,
|
|
111
|
+
"lgtm_streak": 0,
|
|
112
|
+
"paused": false,
|
|
113
|
+
"cost_usd_estimate": 13.8376735,
|
|
114
|
+
"loop": {
|
|
115
|
+
"gate": "reviewer",
|
|
116
|
+
"max_iterations": 2,
|
|
117
|
+
"lgtm_count": 1
|
|
118
|
+
},
|
|
119
|
+
"config": {
|
|
120
|
+
"max_concurrent": 4,
|
|
121
|
+
"model": "gpt-5.4",
|
|
122
|
+
"warn_cost_usd": 50
|
|
123
|
+
},
|
|
124
|
+
"nodes": [
|
|
125
|
+
{
|
|
126
|
+
"id": "l1-methods",
|
|
127
|
+
"type": "research",
|
|
128
|
+
"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…",
|
|
129
|
+
"status": "completed",
|
|
130
|
+
"model": "gpt-5.4",
|
|
131
|
+
"turns": 84,
|
|
132
|
+
"tokens": 3446330,
|
|
133
|
+
"cost_usd_estimate": 1.7322095000000008,
|
|
134
|
+
"produced_outputs": [
|
|
135
|
+
"output/l1-methods.md"
|
|
136
|
+
],
|
|
137
|
+
"outputs": [
|
|
138
|
+
{
|
|
139
|
+
"path": "output/l1-methods.md",
|
|
140
|
+
"kind": "markdown",
|
|
141
|
+
"required": true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"depends_on": [],
|
|
145
|
+
"iterate": true,
|
|
146
|
+
"worktree": false
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "l1-models-data",
|
|
150
|
+
"type": "research",
|
|
151
|
+
"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…",
|
|
152
|
+
"status": "completed",
|
|
153
|
+
"model": "gpt-5.4",
|
|
154
|
+
"turns": 30,
|
|
155
|
+
"tokens": 3795144,
|
|
156
|
+
"cost_usd_estimate": 2.4356045,
|
|
157
|
+
"produced_outputs": [
|
|
158
|
+
"output/l1-models-data.md"
|
|
159
|
+
],
|
|
160
|
+
"outputs": [
|
|
161
|
+
{
|
|
162
|
+
"path": "output/l1-models-data.md",
|
|
163
|
+
"kind": "markdown",
|
|
164
|
+
"required": true
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"depends_on": [],
|
|
168
|
+
"iterate": true,
|
|
169
|
+
"worktree": false
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "l1-economics",
|
|
173
|
+
"type": "research",
|
|
174
|
+
"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…",
|
|
175
|
+
"status": "completed",
|
|
176
|
+
"model": "gpt-5.4",
|
|
177
|
+
"turns": 77,
|
|
178
|
+
"tokens": 2852546,
|
|
179
|
+
"cost_usd_estimate": 1.447467,
|
|
180
|
+
"produced_outputs": [
|
|
181
|
+
"output/l1-economics.md"
|
|
182
|
+
],
|
|
183
|
+
"outputs": [
|
|
184
|
+
{
|
|
185
|
+
"path": "output/l1-economics.md",
|
|
186
|
+
"kind": "markdown",
|
|
187
|
+
"required": true
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"depends_on": [],
|
|
191
|
+
"iterate": true,
|
|
192
|
+
"worktree": false
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"id": "l1-market",
|
|
196
|
+
"type": "research",
|
|
197
|
+
"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…",
|
|
198
|
+
"status": "completed",
|
|
199
|
+
"model": "gpt-5.4",
|
|
200
|
+
"turns": 28,
|
|
201
|
+
"tokens": 1473836,
|
|
202
|
+
"cost_usd_estimate": 1.2037,
|
|
203
|
+
"produced_outputs": [
|
|
204
|
+
"output/l1-market.md"
|
|
205
|
+
],
|
|
206
|
+
"outputs": [
|
|
207
|
+
{
|
|
208
|
+
"path": "output/l1-market.md",
|
|
209
|
+
"kind": "markdown",
|
|
210
|
+
"required": true
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
"depends_on": [],
|
|
214
|
+
"iterate": true,
|
|
215
|
+
"worktree": false
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "l2-deep-methods",
|
|
219
|
+
"type": "research",
|
|
220
|
+
"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…",
|
|
221
|
+
"status": "completed",
|
|
222
|
+
"model": "gpt-5.4",
|
|
223
|
+
"turns": 88,
|
|
224
|
+
"tokens": 5299748,
|
|
225
|
+
"cost_usd_estimate": 1.9066499999999997,
|
|
226
|
+
"produced_outputs": [
|
|
227
|
+
"output/l2-deep-methods.md"
|
|
228
|
+
],
|
|
229
|
+
"outputs": [
|
|
230
|
+
{
|
|
231
|
+
"path": "output/l2-deep-methods.md",
|
|
232
|
+
"kind": "markdown",
|
|
233
|
+
"required": true
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"depends_on": [
|
|
237
|
+
"l1-methods",
|
|
238
|
+
"l1-models-data",
|
|
239
|
+
"l1-economics",
|
|
240
|
+
"l1-market"
|
|
241
|
+
],
|
|
242
|
+
"iterate": true,
|
|
243
|
+
"worktree": false
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "l2-deep-data",
|
|
247
|
+
"type": "research",
|
|
248
|
+
"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…",
|
|
249
|
+
"status": "completed",
|
|
250
|
+
"model": "gpt-5.4",
|
|
251
|
+
"turns": 50,
|
|
252
|
+
"tokens": 5052879,
|
|
253
|
+
"cost_usd_estimate": 2.2376524999999994,
|
|
254
|
+
"produced_outputs": [
|
|
255
|
+
"output/l2-deep-data.md"
|
|
256
|
+
],
|
|
257
|
+
"outputs": [
|
|
258
|
+
{
|
|
259
|
+
"path": "output/l2-deep-data.md",
|
|
260
|
+
"kind": "markdown",
|
|
261
|
+
"required": true
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
"depends_on": [
|
|
265
|
+
"l1-methods",
|
|
266
|
+
"l1-models-data",
|
|
267
|
+
"l1-economics",
|
|
268
|
+
"l1-market"
|
|
269
|
+
],
|
|
270
|
+
"iterate": true,
|
|
271
|
+
"worktree": false
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"id": "l2-deep-pilot",
|
|
275
|
+
"type": "research",
|
|
276
|
+
"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 — …",
|
|
277
|
+
"status": "completed",
|
|
278
|
+
"model": "gpt-5.4",
|
|
279
|
+
"turns": 43,
|
|
280
|
+
"tokens": 3580622,
|
|
281
|
+
"cost_usd_estimate": 2.1080365,
|
|
282
|
+
"produced_outputs": [
|
|
283
|
+
"output/l2-deep-pilot.md"
|
|
284
|
+
],
|
|
285
|
+
"outputs": [
|
|
286
|
+
{
|
|
287
|
+
"path": "output/l2-deep-pilot.md",
|
|
288
|
+
"kind": "markdown",
|
|
289
|
+
"required": true
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
"depends_on": [
|
|
293
|
+
"l1-methods",
|
|
294
|
+
"l1-models-data",
|
|
295
|
+
"l1-economics",
|
|
296
|
+
"l1-market"
|
|
297
|
+
],
|
|
298
|
+
"iterate": true,
|
|
299
|
+
"worktree": false
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"id": "l2-deep-defense",
|
|
303
|
+
"type": "research",
|
|
304
|
+
"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…",
|
|
305
|
+
"status": "completed",
|
|
306
|
+
"model": "gpt-5.4",
|
|
307
|
+
"turns": 19,
|
|
308
|
+
"tokens": 776077,
|
|
309
|
+
"cost_usd_estimate": 0.7663535,
|
|
310
|
+
"produced_outputs": [
|
|
311
|
+
"output/l2-deep-defense.md"
|
|
312
|
+
],
|
|
313
|
+
"outputs": [
|
|
314
|
+
{
|
|
315
|
+
"path": "output/l2-deep-defense.md",
|
|
316
|
+
"kind": "markdown",
|
|
317
|
+
"required": true
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"depends_on": [
|
|
321
|
+
"l1-methods",
|
|
322
|
+
"l1-models-data",
|
|
323
|
+
"l1-economics",
|
|
324
|
+
"l1-market"
|
|
325
|
+
],
|
|
326
|
+
"iterate": true,
|
|
327
|
+
"worktree": false
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"id": "reading-pack",
|
|
331
|
+
"type": "write",
|
|
332
|
+
"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 …",
|
|
333
|
+
"status": "running",
|
|
334
|
+
"model": "gpt-5.4",
|
|
335
|
+
"turns": 0,
|
|
336
|
+
"tokens": 0,
|
|
337
|
+
"cost_usd_estimate": 0,
|
|
338
|
+
"produced_outputs": [],
|
|
339
|
+
"outputs": [
|
|
340
|
+
{
|
|
341
|
+
"path": "output/zero-to-hero.md",
|
|
342
|
+
"kind": "markdown",
|
|
343
|
+
"required": true
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"path": "output/talk-track.md",
|
|
347
|
+
"kind": "markdown",
|
|
348
|
+
"required": true
|
|
349
|
+
}
|
|
350
|
+
],
|
|
351
|
+
"depends_on": [
|
|
352
|
+
"l2-deep-methods",
|
|
353
|
+
"l2-deep-data",
|
|
354
|
+
"l2-deep-pilot",
|
|
355
|
+
"l2-deep-defense"
|
|
356
|
+
],
|
|
357
|
+
"iterate": true,
|
|
358
|
+
"worktree": false
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"id": "gap-reviewer",
|
|
362
|
+
"type": "reviewer",
|
|
363
|
+
"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…",
|
|
364
|
+
"status": "pending",
|
|
365
|
+
"model": "k3",
|
|
366
|
+
"turns": 0,
|
|
367
|
+
"tokens": 0,
|
|
368
|
+
"cost_usd_estimate": 0,
|
|
369
|
+
"produced_outputs": [],
|
|
370
|
+
"outputs": [
|
|
371
|
+
{
|
|
372
|
+
"path": "output/verdict.md",
|
|
373
|
+
"kind": "verdict",
|
|
374
|
+
"required": true
|
|
375
|
+
}
|
|
376
|
+
],
|
|
377
|
+
"depends_on": [
|
|
378
|
+
"reading-pack"
|
|
379
|
+
],
|
|
380
|
+
"iterate": true,
|
|
381
|
+
"worktree": false
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
"edges": [
|
|
385
|
+
{
|
|
386
|
+
"from": "l1-methods",
|
|
387
|
+
"to": "l2-deep-methods"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"from": "l1-models-data",
|
|
391
|
+
"to": "l2-deep-methods"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"from": "l1-economics",
|
|
395
|
+
"to": "l2-deep-methods"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"from": "l1-market",
|
|
399
|
+
"to": "l2-deep-methods"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"from": "l1-methods",
|
|
403
|
+
"to": "l2-deep-data"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"from": "l1-models-data",
|
|
407
|
+
"to": "l2-deep-data"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"from": "l1-economics",
|
|
411
|
+
"to": "l2-deep-data"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"from": "l1-market",
|
|
415
|
+
"to": "l2-deep-data"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"from": "l1-methods",
|
|
419
|
+
"to": "l2-deep-pilot"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"from": "l1-models-data",
|
|
423
|
+
"to": "l2-deep-pilot"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"from": "l1-economics",
|
|
427
|
+
"to": "l2-deep-pilot"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"from": "l1-market",
|
|
431
|
+
"to": "l2-deep-pilot"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"from": "l1-methods",
|
|
435
|
+
"to": "l2-deep-defense"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"from": "l1-models-data",
|
|
439
|
+
"to": "l2-deep-defense"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"from": "l1-economics",
|
|
443
|
+
"to": "l2-deep-defense"
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"from": "l1-market",
|
|
447
|
+
"to": "l2-deep-defense"
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
"from": "l2-deep-methods",
|
|
451
|
+
"to": "reading-pack"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"from": "l2-deep-data",
|
|
455
|
+
"to": "reading-pack"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"from": "l2-deep-pilot",
|
|
459
|
+
"to": "reading-pack"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"from": "l2-deep-defense",
|
|
463
|
+
"to": "reading-pack"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"from": "reading-pack",
|
|
467
|
+
"to": "gap-reviewer"
|
|
468
|
+
}
|
|
469
|
+
],
|
|
470
|
+
"iterations": [],
|
|
471
|
+
"demo": true
|
|
472
|
+
}
|
|
473
|
+
) as Omit<CanvasPayload, "generated_at">;
|
|
474
|
+
|
|
475
|
+
export function buildDemoPayload(): CanvasPayload {
|
|
476
|
+
return { ...DEMO_FLEET, generated_at: new Date().toISOString() };
|
|
477
|
+
}
|
|
478
|
+
|
|
98
479
|
export interface SessionEntryView {
|
|
99
480
|
role: string;
|
|
100
481
|
text: string;
|
|
101
482
|
}
|
|
102
483
|
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
484
|
+
export interface ActionView {
|
|
485
|
+
type: "tool_call" | "tool_result" | "model_change" | "thinking_level_change" | "complete";
|
|
486
|
+
name?: string;
|
|
487
|
+
toolName?: string;
|
|
488
|
+
arguments?: Record<string, unknown>;
|
|
489
|
+
provider?: string;
|
|
490
|
+
modelId?: string;
|
|
491
|
+
thinkingLevel?: string;
|
|
492
|
+
stopReason?: string;
|
|
493
|
+
isError?: boolean;
|
|
494
|
+
timestamp?: string;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export type TimelineEvent =
|
|
498
|
+
| { type: "message"; role: string; text: string; timestamp?: string }
|
|
499
|
+
| { type: "tool_call"; name: string; arguments?: Record<string, unknown>; timestamp?: string }
|
|
500
|
+
| { type: "tool_result"; toolName?: string; isError?: boolean; text?: string; timestamp?: string }
|
|
501
|
+
| { type: "model_change"; provider: string; modelId: string; timestamp?: string }
|
|
502
|
+
| { type: "thinking_level_change"; thinkingLevel: string; timestamp?: string }
|
|
503
|
+
| { type: "complete"; stopReason: string; timestamp?: string };
|
|
504
|
+
|
|
505
|
+
export interface SessionTailView {
|
|
506
|
+
entries: SessionEntryView[];
|
|
507
|
+
actions: ActionView[];
|
|
508
|
+
events: TimelineEvent[];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function parseSessionTail(jsonl: string, maxEntries: number): SessionTailView {
|
|
512
|
+
const events: TimelineEvent[] = [];
|
|
513
|
+
const entries: SessionEntryView[] = [];
|
|
514
|
+
const actions: ActionView[] = [];
|
|
515
|
+
for (const raw of jsonl.split("\n")) {
|
|
516
|
+
if (!raw.trim()) continue;
|
|
517
|
+
let e: { type?: string; timestamp?: string; message?: { role?: unknown; content?: unknown; stopReason?: string; toolName?: string; isError?: boolean }; provider?: string; modelId?: string; thinkingLevel?: string } | undefined;
|
|
518
|
+
try { e = JSON.parse(raw); } catch { continue; }
|
|
519
|
+
if (!e) continue;
|
|
520
|
+
const ts = typeof e.timestamp === "string" ? e.timestamp : undefined;
|
|
521
|
+
if (e.type === "model_change" && typeof e.provider === "string" && typeof e.modelId === "string") {
|
|
522
|
+
const a: ActionView = { type: "model_change", provider: e.provider, modelId: e.modelId, timestamp: ts };
|
|
523
|
+
actions.push(a);
|
|
524
|
+
events.push({ type: "model_change", provider: e.provider, modelId: e.modelId, timestamp: ts });
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (e.type === "thinking_level_change" && typeof e.thinkingLevel === "string") {
|
|
528
|
+
const a: ActionView = { type: "thinking_level_change", thinkingLevel: e.thinkingLevel, timestamp: ts };
|
|
529
|
+
actions.push(a);
|
|
530
|
+
events.push({ type: "thinking_level_change", thinkingLevel: e.thinkingLevel, timestamp: ts });
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (e.type !== "message") continue;
|
|
534
|
+
const msg = e.message;
|
|
535
|
+
if (!msg || typeof msg.role !== "string" || !Array.isArray(msg.content)) continue;
|
|
536
|
+
// tool result messages carry the result at the message level
|
|
537
|
+
if (msg.role === "toolResult" || msg.role === "tool_result") {
|
|
538
|
+
const a: ActionView = { type: "tool_result", toolName: typeof msg.toolName === "string" ? msg.toolName : undefined, isError: msg.isError, timestamp: ts };
|
|
539
|
+
actions.push(a);
|
|
540
|
+
events.push({ type: "tool_result", toolName: a.toolName, isError: a.isError, timestamp: ts });
|
|
541
|
+
}
|
|
542
|
+
const parts: string[] = [];
|
|
543
|
+
for (const p of msg.content as Array<{ type?: string; text?: string; name?: string; toolName?: string; isError?: boolean; arguments?: Record<string, unknown> }>) {
|
|
544
|
+
if (!p) continue;
|
|
545
|
+
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
546
|
+
else if ((p.type === "toolCall" || p.type === "tool_call") && typeof p.name === "string") {
|
|
547
|
+
parts.push(`[tool: ${p.name}]`);
|
|
548
|
+
const a: ActionView = { type: "tool_call", name: p.name, arguments: p.arguments, timestamp: ts };
|
|
549
|
+
actions.push(a);
|
|
550
|
+
events.push({ type: "tool_call", name: p.name, arguments: p.arguments, timestamp: ts });
|
|
551
|
+
} else if (p.type === "toolResult" || p.type === "tool_result") {
|
|
552
|
+
parts.push("[tool result]");
|
|
553
|
+
const a: ActionView = { type: "tool_result", toolName: typeof p.toolName === "string" ? p.toolName : p.name, isError: p.isError, timestamp: ts };
|
|
554
|
+
actions.push(a);
|
|
555
|
+
events.push({ type: "tool_result", toolName: a.toolName, isError: a.isError, timestamp: ts });
|
|
120
556
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
557
|
+
}
|
|
558
|
+
if (msg.stopReason && typeof msg.stopReason === "string" && msg.stopReason !== "toolUse" && msg.stopReason !== "tool_use") {
|
|
559
|
+
const a: ActionView = { type: "complete", stopReason: msg.stopReason, timestamp: ts };
|
|
560
|
+
actions.push(a);
|
|
561
|
+
events.push({ type: "complete", stopReason: msg.stopReason, timestamp: ts });
|
|
562
|
+
}
|
|
563
|
+
const text = parts.join("\n").trim();
|
|
564
|
+
if (text.length > 0) {
|
|
565
|
+
const entry: SessionEntryView = { role: msg.role as string, text: text.length > 4000 ? `${text.slice(0, 4000)}…` : text };
|
|
566
|
+
entries.push(entry);
|
|
567
|
+
events.push({ type: "message", role: entry.role, text: entry.text, timestamp: ts });
|
|
123
568
|
}
|
|
124
569
|
}
|
|
125
|
-
return
|
|
570
|
+
return { entries: entries.slice(-maxEntries), actions: actions.slice(-maxEntries), events: events.slice(-maxEntries) };
|
|
126
571
|
}
|
|
127
572
|
|
|
573
|
+
const DEMO_TASK = `You are L1 (lay-of-the-land) researcher in a 2-layer research fleet.\n\nMission context: founder built QuickCall — a daemon watching engineers' AI coding-agent sessions (Claude Code, Cursor, Codex), extracting team conventions, capturing accept/reject signals and human corrections on agent output.\n\nYour job:\n1. Read the mission brief and upstream inputs.\n2. Search for relevant precedents, code patterns, and competitive landscape.\n3. Write a concise markdown report to the required output path.\n4. Save ALL output files to the worker output directory using absolute paths.\n\nSave the report to output/l1-methods.md. The file must use markdown headings and keep each section focused. Do not modify source code.`;
|
|
574
|
+
|
|
575
|
+
export function buildDemoSession(_id: string, task?: string): SessionTailView & { task: string } {
|
|
576
|
+
const taskText = task || DEMO_TASK;
|
|
577
|
+
const baseTs = "2026-08-01T09:57:";
|
|
578
|
+
const events: TimelineEvent[] = [
|
|
579
|
+
{ type: "model_change", provider: "openai-codex", modelId: "gpt-5.4-mini", timestamp: baseTs + "04.987Z" },
|
|
580
|
+
{ type: "thinking_level_change", thinkingLevel: "high", timestamp: baseTs + "04.987Z" },
|
|
581
|
+
{ type: "message", role: "assistant", text: "I'll review the scheduler changes and write the verdict.", timestamp: baseTs + "05.000Z" },
|
|
582
|
+
{ type: "tool_call", name: "read", arguments: { path: "docs/superpowers/specs/reviewer-contract.md" }, timestamp: baseTs + "06.000Z" },
|
|
583
|
+
{ type: "tool_call", name: "read", arguments: { path: "src/scheduler.ts" }, timestamp: baseTs + "06.500Z" },
|
|
584
|
+
{ type: "message", role: "assistant", text: "The scheduler uses a priority queue. I'll run the test suite to verify behavior.", timestamp: baseTs + "08.000Z" },
|
|
585
|
+
{ type: "tool_call", name: "bash", arguments: { command: "npm run typecheck" }, timestamp: baseTs + "09.000Z" },
|
|
586
|
+
{ type: "tool_result", toolName: "bash", isError: false, text: "✓ typecheck passed", timestamp: baseTs + "12.000Z" },
|
|
587
|
+
{ type: "message", role: "assistant", text: "Typecheck passes. I'll search for fleet reviewer patterns and inspect tests.", timestamp: baseTs + "12.500Z" },
|
|
588
|
+
{ type: "tool_call", name: "web_search", arguments: { queries: ["fleet reviewer DAG pattern"] }, timestamp: baseTs + "13.000Z" },
|
|
589
|
+
{ type: "tool_call", name: "read", arguments: { path: "test/scheduler.test.ts" }, timestamp: baseTs + "15.000Z" },
|
|
590
|
+
{ type: "tool_result", toolName: "read", isError: false, text: "# 123", timestamp: baseTs + "15.500Z" },
|
|
591
|
+
{ type: "message", role: "assistant", text: "Upstream outputs look good. Tests pass and search results confirm the pattern. Writing the review verdict now.", timestamp: baseTs + "17.000Z" },
|
|
592
|
+
{ type: "tool_call", name: "write", arguments: { path: "output/review.md" }, timestamp: baseTs + "18.000Z" },
|
|
593
|
+
{ type: "tool_result", toolName: "write", isError: false, text: "Successfully wrote 45 bytes to output/review.md", timestamp: baseTs + "18.500Z" },
|
|
594
|
+
{ type: "message", role: "assistant", text: "Done.", timestamp: baseTs + "19.000Z" },
|
|
595
|
+
{ type: "complete", stopReason: "complete", timestamp: baseTs + "20.000Z" },
|
|
596
|
+
];
|
|
597
|
+
const entries: SessionEntryView[] = events.filter((e) => e.type === "message").map((e) => ({ role: (e as TimelineEvent & { type: "message" }).role, text: (e as TimelineEvent & { type: "message" }).text }));
|
|
598
|
+
const actions: ActionView[] = events.filter((e) => e.type !== "message").map((e) => {
|
|
599
|
+
if (e.type === "tool_call") return { type: e.type, name: e.name, arguments: e.arguments, timestamp: e.timestamp };
|
|
600
|
+
if (e.type === "tool_result") return { type: e.type, toolName: e.toolName, isError: e.isError, timestamp: e.timestamp };
|
|
601
|
+
if (e.type === "model_change") return { type: e.type, provider: e.provider, modelId: e.modelId, timestamp: e.timestamp };
|
|
602
|
+
if (e.type === "thinking_level_change") return { type: e.type, thinkingLevel: e.thinkingLevel, timestamp: e.timestamp };
|
|
603
|
+
return { type: e.type, stopReason: e.stopReason, timestamp: e.timestamp };
|
|
604
|
+
}) as ActionView[];
|
|
605
|
+
return { entries, actions, events, task: taskText };
|
|
606
|
+
}
|
|
128
607
|
async function latestSessionFile(workerDir: string): Promise<string | undefined> {
|
|
129
608
|
try {
|
|
130
609
|
const files = (await readdir(workerDir)).filter((f) => f.endsWith(".jsonl")).sort();
|
|
@@ -134,396 +613,241 @@ async function latestSessionFile(workerDir: string): Promise<string | undefined>
|
|
|
134
613
|
}
|
|
135
614
|
}
|
|
136
615
|
|
|
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);
|
|
211
|
-
|
|
212
|
-
function j(u){ return fetch(u).then(function(r){ if(!r.ok) throw new Error(String(r.status)); return r.json(); }); }
|
|
213
|
-
function esc(s){ return String(s).replace(/[&<>"]/g, function(c){ return { "&":"&", "<":"<", ">":">", '"':""" }[c]; }); }
|
|
616
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
617
|
+
const requireFrom = createRequire(import.meta.url);
|
|
214
618
|
|
|
215
|
-
|
|
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
|
-
})();
|
|
619
|
+
let bundleCache: Promise<string> | undefined;
|
|
230
620
|
|
|
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);
|
|
621
|
+
/** Bundle the React canvas client with esbuild (cached after first build). */
|
|
622
|
+
async function buildClientBundle(): Promise<string> {
|
|
623
|
+
const esbuild = await import("esbuild");
|
|
624
|
+
const result = await esbuild.build({
|
|
625
|
+
entryPoints: [join(HERE, "canvas-client.tsx")],
|
|
626
|
+
bundle: true,
|
|
627
|
+
format: "iife",
|
|
628
|
+
platform: "browser",
|
|
629
|
+
target: "es2020",
|
|
630
|
+
jsx: "automatic",
|
|
631
|
+
minify: true,
|
|
632
|
+
write: false,
|
|
633
|
+
logLevel: "silent",
|
|
634
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
270
635
|
});
|
|
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();
|
|
636
|
+
return result.outputFiles[0].text;
|
|
277
637
|
}
|
|
278
|
-
function resetView(){ camera = { x: 40, y: 40, scale: 1 }; applyCamera(); }
|
|
279
638
|
|
|
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]; });
|
|
639
|
+
function flowCss(): string {
|
|
640
|
+
try {
|
|
641
|
+
return readFileSyncSafe(requireFrom.resolve("@xyflow/react/dist/style.css"));
|
|
642
|
+
} catch {
|
|
643
|
+
return "";
|
|
293
644
|
}
|
|
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
645
|
}
|
|
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);
|
|
646
|
+
function readFileSyncSafe(p: string): string {
|
|
647
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
648
|
+
return (requireFrom("node:fs") as typeof import("node:fs")).readFileSync(p, "utf-8");
|
|
367
649
|
}
|
|
368
650
|
|
|
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
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
651
|
+
const PAGE_CSS = `
|
|
652
|
+
: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); }
|
|
653
|
+
[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); }
|
|
654
|
+
:root { --ui:system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --mono:"SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; }
|
|
655
|
+
* { box-sizing:border-box; }
|
|
656
|
+
html,body,#root { margin:0; height:100%; }
|
|
657
|
+
html,body { overflow:hidden; font:13px/1.45 var(--ui); background:var(--bg); color:var(--fg); }
|
|
658
|
+
/* monospace is reserved for identifiers, code paths, measurements, and transcripts */
|
|
659
|
+
.id, .stats, .out-chip, .badge, .fp-name, .fp-status, .fp-trigger-status, .taskbox-side, .msg, .empty code { font-family:var(--mono); }
|
|
660
|
+
#root { display:flex; flex-direction:column; }
|
|
661
|
+
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; }
|
|
662
|
+
header .name { font-weight:700; color:var(--hdr); }
|
|
663
|
+
#hdr { display:flex; flex-wrap:wrap; gap:6px 12px; align-items:center; min-width:0; }
|
|
664
|
+
.pill { padding:1px 8px; border-radius:10px; border:1px solid var(--line); }
|
|
665
|
+
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; }
|
|
666
|
+
button:hover, select:hover { border-color:var(--accent); }
|
|
667
|
+
:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:6px; }
|
|
668
|
+
.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); }
|
|
669
|
+
.icon-btn:hover { color:var(--fg); }
|
|
670
|
+
.fp { position:relative; }
|
|
671
|
+
.fp-trigger { display:flex; align-items:center; gap:7px; min-width:210px; max-width:360px; padding:5px 11px; min-height:30px; text-align:left; }
|
|
672
|
+
.fp-trigger-status { font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:8px; padding:0 6px; white-space:nowrap; }
|
|
673
|
+
.fp-label { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
674
|
+
.fp-caret { color:var(--muted); font-size:11px; }
|
|
675
|
+
.dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; background:var(--muted); }
|
|
676
|
+
.dot.live { background:transparent; border:2px solid var(--accent); }
|
|
677
|
+
.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; }
|
|
678
|
+
.fp-search { width:100%; border:none; border-bottom:1px solid var(--line); border-radius:0; padding:9px 12px; background:transparent; color:var(--fg); }
|
|
679
|
+
.fp-search:focus-visible { outline-offset:-2px; }
|
|
680
|
+
.fp-list { max-height:340px; overflow-y:auto; padding:4px 0; }
|
|
681
|
+
.fp-item { display:flex; align-items:center; gap:9px; padding:8px 12px; min-height:34px; cursor:pointer; }
|
|
682
|
+
.fp-item.active { background:color-mix(in srgb, var(--fg) 8%, transparent); }
|
|
683
|
+
.fp-item.selected .fp-name { color:var(--accent); font-weight:600; }
|
|
684
|
+
.fp-name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
685
|
+
.fp-status { font-size:11px; color:var(--muted); white-space:nowrap; flex-shrink:0; }
|
|
686
|
+
.fp-empty { padding:10px; color:var(--muted); text-align:center; }
|
|
687
|
+
.taskbox-side-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:0.04em; margin-bottom:4px; }
|
|
688
|
+
main { display:flex; flex:1 1 auto; min-height:0; }
|
|
689
|
+
#stage { flex:1; position:relative; min-width:0; background:var(--stage-bg); }
|
|
690
|
+
.react-flow { background:var(--stage-bg); }
|
|
691
|
+
.react-flow__node { width:284px; }
|
|
692
|
+
.react-flow__handle { width:6px; height:6px; background:var(--wire); border:none; opacity:0; }
|
|
693
|
+
.react-flow__edge-path { stroke:var(--edge); stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
|
|
694
|
+
.react-flow__edge:hover .react-flow__edge-path { stroke:var(--accent); stroke-width:2; }
|
|
695
|
+
.react-flow__edge.animated .react-flow__edge-path { stroke:var(--accent); stroke-width:2; stroke-dasharray:1 7; animation:dashflow 0.7s linear infinite; }
|
|
696
|
+
@keyframes dashflow { to { stroke-dashoffset:-16; } }
|
|
697
|
+
.react-flow__controls button { background:var(--panel); color:var(--fg); border-bottom:1px solid var(--line); fill:var(--fg); }
|
|
698
|
+
.react-flow__minimap { width:172px; height:112px; background:var(--panel); border-radius:12px; box-shadow:var(--card-shadow); overflow:hidden; }
|
|
699
|
+
.react-flow__minimap svg { border-radius:10px; }
|
|
700
|
+
.react-flow__minimap-mask { fill:var(--mm-mask) !important; stroke:none; }
|
|
701
|
+
.react-flow__minimap-node { stroke:var(--panel) !important; stroke-width:4px !important; rx:3; ry:3; }
|
|
702
|
+
.react-flow__attribution { display:none; }
|
|
703
|
+
.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; }
|
|
704
|
+
.node:hover { box-shadow:var(--card-shadow-lg); transform:translateY(-2px); }
|
|
705
|
+
.node.sel { background:color-mix(in srgb, var(--accent) 14%, var(--panel)); box-shadow:var(--card-shadow-lg); transform:translateY(-2px); }
|
|
706
|
+
.node.sel.st-failed, .node.sel.st-contract_failed { background:color-mix(in srgb, var(--bad) 14%, var(--panel)); }
|
|
707
|
+
/* keyboard focus ring only — never on mouse click */
|
|
708
|
+
.node:focus:not(:focus-visible) { outline:none; }
|
|
709
|
+
.node.st-running { background:color-mix(in srgb, var(--accent) 10%, var(--panel)); }
|
|
710
|
+
.node.st-failed, .node.st-contract_failed { background:color-mix(in srgb, var(--bad) 11%, var(--panel)); }
|
|
711
|
+
.card-body { padding:13px 14px; }
|
|
712
|
+
.node-header { display:flex; align-items:center; gap:7px; }
|
|
713
|
+
.node-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
714
|
+
.node-dot.pulse { animation:pulse 1.6s ease-in-out infinite; }
|
|
715
|
+
.node-header .id { flex:1; }
|
|
716
|
+
.node-header .badge { margin-left:auto; }
|
|
717
|
+
.id { font-weight:700; font-size:16px; letter-spacing:-0.01em; color:var(--hdr); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
718
|
+
.badge { font-size:11px; padding:1px 7px; border:1px solid var(--line); border-radius:10px; color:var(--muted); white-space:nowrap; }
|
|
719
|
+
.status-row { margin-top:7px; display:flex; align-items:center; gap:6px; font-size:13px; color:var(--muted); }
|
|
720
|
+
.status-row .st-word { color:var(--fg); font-weight:600; }
|
|
721
|
+
.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; }
|
|
722
|
+
@keyframes spin { to { transform:rotate(360deg); } }
|
|
723
|
+
.stats { margin-top:4px; font-size:13px; color:var(--muted); }
|
|
724
|
+
.outputs { margin-top:6px; }
|
|
725
|
+
.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); }
|
|
726
|
+
.flags { margin-top:6px; font-size:11px; color:var(--warn); }
|
|
727
|
+
.flags span { cursor:help; }
|
|
728
|
+
.note { margin-top:6px; font-size:13px; color:var(--warn); }
|
|
729
|
+
#side { width:420px; flex:0 0 auto; border-left:1px solid var(--line); overflow:hidden; display:flex; flex-direction:column; background:var(--bg); }
|
|
730
|
+
#side:focus, #side:focus-visible { outline:none; }
|
|
731
|
+
.side-head { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; flex-shrink:0; gap:6px; padding:12px 12px 8px; border-bottom:1px solid var(--line); background:var(--bg); }
|
|
732
|
+
.side-head .side-meta { display:flex; gap:6px; width:100%; }
|
|
733
|
+
.side-head .side-meta-chip { font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:4px; padding:2px 6px; white-space:nowrap; }
|
|
734
|
+
.side-head .meta { color:var(--muted); font-size:12px; display:flex; align-items:center; gap:4px; }
|
|
735
|
+
.side-head .side-hash { color:var(--muted); font-weight:400; }
|
|
736
|
+
.side-head .side-id { font-weight:700; color:var(--hdr); font-size:15px; letter-spacing:-0.01em; }
|
|
737
|
+
.side-body { flex:1; overflow:auto; padding:8px 12px 12px; }
|
|
738
|
+
.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; }
|
|
739
|
+
.msg { margin-bottom:10px; padding:8px; border-radius:6px; background:var(--panel); white-space:pre-wrap; word-break:break-word; }
|
|
740
|
+
.msg .role { font-weight:700; margin-bottom:4px; }
|
|
741
|
+
.role-user { color:var(--accent); } .role-assistant { color:var(--ok); } .role-tool { color:var(--warn); }
|
|
742
|
+
.collapsible { position:sticky; top:8px; z-index:1; border:1px solid var(--line); border-radius:6px; margin-bottom:10px; background:var(--bg); }
|
|
743
|
+
.collapsible-head { display:flex; align-items:center; gap:6px; width:100%; padding:8px; background:transparent; border:none; font:inherit; font-size:12px; color:var(--muted); cursor:pointer; text-align:left; }
|
|
744
|
+
.collapsible-head:hover { background:var(--panel); }
|
|
745
|
+
.collapsible-body { padding:8px; border-top:1px solid var(--line); white-space:pre-wrap; word-break:break-word; font-size:13px; max-height:260px; overflow:auto; }
|
|
746
|
+
.collapsible.collapsed .collapsible-body { display:none; }
|
|
747
|
+
.collapsible .chevron { display:inline-block; transition:transform .15s; }
|
|
748
|
+
.collapsible.collapsed .chevron { transform:rotate(-90deg); }
|
|
749
|
+
.timeline { margin-bottom:12px; }
|
|
750
|
+
.timeline-action { background:var(--panel); border-radius:5px; margin-bottom:5px; }
|
|
751
|
+
.timeline-action.open { background:var(--panel-2); }
|
|
752
|
+
.timeline-action.action-error { border-left:3px solid var(--bad); background:color-mix(in srgb, var(--bad) 9%, var(--panel)); }
|
|
753
|
+
.timeline-row { display:flex; align-items:center; gap:8px; padding:5px 8px; font-size:12px; }
|
|
754
|
+
.timeline-row .action-icon { flex-shrink:0; width:34px; text-align:center; color:var(--accent); font-family:var(--mono); font-size:11px; text-transform:uppercase; letter-spacing:0.02em; }
|
|
755
|
+
.timeline-row .action-name { flex-shrink:0; font-weight:600; min-width:48px; font-size:12px; white-space:nowrap; }
|
|
756
|
+
.timeline-row .action-detail { flex:1; min-width:0; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; }
|
|
757
|
+
.timeline-row .action-error { color:var(--bad); }
|
|
758
|
+
.timeline-row .ts { flex-shrink:0; color:var(--wire); font-size:11px; font-variant-numeric:tabular-nums; }
|
|
759
|
+
.timeline-msg { padding:8px; margin-bottom:6px; border-radius:5px; background:var(--panel); font-size:13px; }
|
|
760
|
+
.timeline-msg.user { border-left:1px solid var(--accent); background:color-mix(in srgb, var(--accent) 5%, var(--panel)); }
|
|
761
|
+
.timeline-msg.assistant { border-left:1px solid var(--ok); background:color-mix(in srgb, var(--ok) 5%, var(--panel)); }
|
|
762
|
+
.timeline-msg .role { font-weight:700; margin-bottom:4px; }
|
|
763
|
+
.timeline-msg .timeline-meta { display:flex; justify-content:space-between; align-items:center; margin-bottom:3px; }
|
|
764
|
+
.timeline-msg .timeline-meta .ts { color:var(--wire); font-size:11px; }
|
|
765
|
+
.timeline-msg .timeline-text { white-space:pre-wrap; word-break:break-word; }
|
|
766
|
+
.timeline-loading { display:flex; align-items:center; justify-content:center; gap:8px; padding:16px 8px; color:var(--muted); font-size:13px; }
|
|
767
|
+
.timeline-empty { padding:16px 8px; color:var(--muted); font-size:13px; text-align:center; }
|
|
768
|
+
.activity-toggle { flex-shrink:0; width:18px; height:18px; padding:0; background:transparent; border:1px solid var(--line); border-radius:4px; color:var(--muted); cursor:pointer; font-size:11px; line-height:1; }
|
|
769
|
+
.activity-toggle:hover { background:var(--bg); color:var(--fg); }
|
|
770
|
+
.activity-body { padding:8px; border-top:1px solid var(--line); font-size:12px; color:var(--muted); font-family:var(--mono); white-space:pre-wrap; word-break:break-word; }
|
|
771
|
+
.activity-body pre { margin:0; background:var(--bg); padding:8px; border-radius:4px; overflow:auto; font-size:11px; }
|
|
772
|
+
.react-flow__minimap-node.st-completed { fill:var(--ok); }
|
|
773
|
+
.react-flow__minimap-node.st-running { fill:var(--accent); }
|
|
774
|
+
.react-flow__minimap-node.st-failed, .react-flow__minimap-node.st-contract_failed { fill:var(--bad); }
|
|
775
|
+
.react-flow__minimap-node.st-killed, .react-flow__minimap-node.st-blocked { fill:var(--wire); }
|
|
776
|
+
.react-flow__minimap-node.st-pending, .react-flow__minimap-node.st-ready { fill:var(--line); }
|
|
777
|
+
/* header status strip */
|
|
778
|
+
.spacer { flex:1; }
|
|
779
|
+
.fleet-title { font-weight:700; font-size:16px; color:var(--hdr); max-width:34ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
780
|
+
.stat { display:inline-flex; align-items:center; gap:5px; color:var(--muted); }
|
|
781
|
+
.pill.status-running { color:var(--accent); border-color:var(--accent); }
|
|
782
|
+
.pill.status-completed { color:var(--ok); border-color:var(--ok); }
|
|
783
|
+
.pill.status-failed, .pill.status-contract_failed { color:var(--bad); border-color:var(--bad); }
|
|
784
|
+
.pill-bad { color:var(--bad); border-color:var(--bad); }
|
|
785
|
+
.pill-btn { display:inline-flex; align-items:center; min-height:24px; cursor:pointer; font:inherit; padding:3px 9px; }
|
|
786
|
+
.pill-btn:hover { background:var(--bg); }
|
|
787
|
+
.dot-run { background:var(--accent); animation:pulse 1.6s ease-in-out infinite; }
|
|
788
|
+
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.35; } }
|
|
789
|
+
.conn { display:inline-flex; align-items:center; gap:8px; }
|
|
790
|
+
.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; }
|
|
791
|
+
button.toggled { border-color:var(--accent); color:var(--accent); }
|
|
792
|
+
/* legend */
|
|
793
|
+
.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; }
|
|
794
|
+
.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); }
|
|
795
|
+
.legend-row { display:flex; align-items:center; gap:9px; padding:2px 0; }
|
|
796
|
+
.swatch { width:12px; height:12px; border-radius:3px; flex-shrink:0; background:var(--line); }
|
|
797
|
+
.swatch.st-completed { background:var(--ok); }
|
|
798
|
+
.swatch.st-running { background:var(--accent); }
|
|
799
|
+
.swatch.st-failed { background:var(--bad); }
|
|
800
|
+
.swatch.st-blocked { background:var(--wire); }
|
|
801
|
+
.swatch.st-pending { background:var(--line); }
|
|
802
|
+
.swatch-line { width:16px; height:0; border-top:2px dashed var(--warn); flex-shrink:0; }
|
|
803
|
+
.legend-loop { color:var(--warn); margin-top:2px; }
|
|
804
|
+
.icon-btn.sm { width:24px; height:24px; min-height:24px; font-size:16px; }
|
|
805
|
+
/* empty state */
|
|
806
|
+
.empty { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; text-align:center; padding:24px; }
|
|
807
|
+
.empty-title { font-size:19px; font-weight:700; color:var(--hdr); }
|
|
808
|
+
.empty-body { max-width:460px; margin:0; color:var(--muted); }
|
|
809
|
+
.empty-steps { max-width:460px; margin:0; text-align:left; color:var(--muted); line-height:1.8; padding-left:18px; }
|
|
810
|
+
.empty code { background:var(--panel); border:1px solid var(--line); border-radius:4px; padding:1px 5px; }
|
|
811
|
+
.empty-cta { border-color:var(--accent); color:var(--accent); padding:8px 16px; }
|
|
812
|
+
.empty-cta:hover { background:var(--panel); }
|
|
813
|
+
/* failure reason surfaced on failed cards */
|
|
814
|
+
.fail-reason { margin-top:6px; font-size:13px; color:var(--bad); }
|
|
815
|
+
/* respect reduced-motion: keep the state, drop the perpetual movement */
|
|
816
|
+
@media (prefers-reduced-motion: reduce) {
|
|
817
|
+
.spinner { animation:none; border-top-color:var(--accent); opacity:0.6; }
|
|
818
|
+
.dot-run, .node-dot.pulse { animation:none; }
|
|
819
|
+
.node:hover { transform:none; }
|
|
820
|
+
.react-flow__edge.animated .react-flow__edge-path { animation:none; }
|
|
821
|
+
* { scroll-behavior:auto; }
|
|
423
822
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
r.fleets.forEach(function(f){
|
|
431
|
-
opts.push('<option value="' + esc(f.name) + '">' + esc(f.name) + ' (' + esc(f.status) + ')</option>');
|
|
432
|
-
});
|
|
433
|
-
selEl.innerHTML = opts.join("");
|
|
434
|
-
var qs = new URLSearchParams(location.search);
|
|
435
|
-
var want = qs.get("fleet");
|
|
436
|
-
var saved = null;
|
|
437
|
-
try { saved = localStorage.getItem("fleet-canvas-fleet"); } catch(e) {}
|
|
438
|
-
var pick = want || saved || "";
|
|
439
|
-
if(pick && r.fleets.some(function(f){ return f.name === pick; })){
|
|
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
|
-
});
|
|
823
|
+
/* narrow viewports: side panel overlays the stage, header controls stay reachable */
|
|
824
|
+
@media (max-width:700px) {
|
|
825
|
+
.spacer { display:none; }
|
|
826
|
+
#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); }
|
|
827
|
+
.fp-trigger { min-width:150px; }
|
|
828
|
+
.legend { bottom:auto; top:12px; }
|
|
453
829
|
}
|
|
454
|
-
|
|
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
|
-
});
|
|
830
|
+
`;
|
|
461
831
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
832
|
+
/** Full canvas HTML page with the React/@xyflow bundle inlined. Cached after first build. */
|
|
833
|
+
export async function renderCanvasPage(): Promise<string> {
|
|
834
|
+
bundleCache ??= buildClientBundle();
|
|
835
|
+
const [bundle] = await Promise.all([bundleCache]);
|
|
836
|
+
return `<!doctype html>
|
|
837
|
+
<html>
|
|
838
|
+
<head>
|
|
839
|
+
<meta charset="utf-8">
|
|
840
|
+
<title>fleet canvas</title>
|
|
841
|
+
<style>${flowCss()}</style>
|
|
842
|
+
<style>${PAGE_CSS}</style>
|
|
843
|
+
</head>
|
|
844
|
+
<body>
|
|
845
|
+
<div id="root"></div>
|
|
846
|
+
<script>${bundle}</script>
|
|
466
847
|
</body>
|
|
467
848
|
</html>`;
|
|
468
849
|
}
|
|
469
850
|
|
|
470
|
-
export interface FleetRootInfo {
|
|
471
|
-
name: string;
|
|
472
|
-
root: string;
|
|
473
|
-
status: string;
|
|
474
|
-
created_at: string;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
export async function readDiskFleet(fleetRoot: string): Promise<ActiveFleet> {
|
|
478
|
-
const spec = JSON.parse(await readFile(join(fleetRoot, "fleet.json"), "utf-8")) as FleetSpec;
|
|
479
|
-
const state = await readState(fleetRoot);
|
|
480
|
-
return {
|
|
481
|
-
spec,
|
|
482
|
-
fleetRoot,
|
|
483
|
-
state,
|
|
484
|
-
killSwitch: { killed: false },
|
|
485
|
-
pauseSwitch: { paused: false },
|
|
486
|
-
running: false,
|
|
487
|
-
sessions: new Map(),
|
|
488
|
-
killedNodes: new Set(),
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
export async function listFleetRoots(cwd: string): Promise<FleetRootInfo[]> {
|
|
493
|
-
const base = join(cwd, ".fleet");
|
|
494
|
-
let entries: string[];
|
|
495
|
-
try {
|
|
496
|
-
entries = await readdir(base);
|
|
497
|
-
} catch {
|
|
498
|
-
return [];
|
|
499
|
-
}
|
|
500
|
-
const out: FleetRootInfo[] = [];
|
|
501
|
-
for (const name of entries) {
|
|
502
|
-
const root = join(base, name);
|
|
503
|
-
try {
|
|
504
|
-
const s = await stat(join(root, "fleet.json"));
|
|
505
|
-
if (!s.isFile()) continue;
|
|
506
|
-
const state = JSON.parse(await readFile(join(root, "state.json"), "utf-8")) as Partial<FleetState>;
|
|
507
|
-
out.push({
|
|
508
|
-
name,
|
|
509
|
-
root,
|
|
510
|
-
status: typeof state.status === "string" ? state.status : "unknown",
|
|
511
|
-
created_at: typeof state.created_at === "string" ? state.created_at : new Date(s.mtimeMs).toISOString(),
|
|
512
|
-
});
|
|
513
|
-
} catch {
|
|
514
|
-
// not a fleet root (no fleet.json) or unreadable state — skip or mark unknown
|
|
515
|
-
try {
|
|
516
|
-
await stat(join(root, "fleet.json"));
|
|
517
|
-
out.push({ name, root, status: "unknown", created_at: "" });
|
|
518
|
-
} catch {
|
|
519
|
-
// not a fleet root
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
out.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
524
|
-
return out;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
851
|
export interface CanvasServer {
|
|
528
852
|
url: string;
|
|
529
853
|
port: number;
|
|
@@ -540,7 +864,12 @@ export async function startCanvasServer(opts: {
|
|
|
540
864
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
541
865
|
if (url.pathname === "/") {
|
|
542
866
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
543
|
-
res.end(renderCanvasPage());
|
|
867
|
+
res.end(await renderCanvasPage());
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (url.pathname === "/api/demo") {
|
|
871
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
872
|
+
res.end(JSON.stringify(buildDemoPayload()));
|
|
544
873
|
return;
|
|
545
874
|
}
|
|
546
875
|
const resolveFleet = async (name: string | null): Promise<ActiveFleet | undefined | "unknown"> => {
|
|
@@ -573,6 +902,14 @@ export async function startCanvasServer(opts: {
|
|
|
573
902
|
}
|
|
574
903
|
const m = url.pathname.match(/^\/api\/session\/([a-z0-9][a-z0-9-]*)$/);
|
|
575
904
|
if (m) {
|
|
905
|
+
const isDemo = url.searchParams.get("demo") === "1";
|
|
906
|
+
let workerTask: string | undefined;
|
|
907
|
+
if (isDemo) {
|
|
908
|
+
const demo = buildDemoSession(m[1]);
|
|
909
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
910
|
+
res.end(JSON.stringify(demo));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
576
913
|
const f = await resolveFleet(url.searchParams.get("fleet"));
|
|
577
914
|
if (!f || f === "unknown" || !f.spec.workers.some((w) => w.id === m[1])) {
|
|
578
915
|
res.writeHead(404);
|
|
@@ -584,12 +921,14 @@ export async function startCanvasServer(opts: {
|
|
|
584
921
|
const file = await latestSessionFile(join(f.fleetRoot, "workers", m[1]));
|
|
585
922
|
res.writeHead(200, { "content-type": "application/json" });
|
|
586
923
|
const worker = f.spec.workers.find((w) => w.id === m[1]);
|
|
924
|
+
workerTask = worker?.task;
|
|
587
925
|
if (!file) {
|
|
588
|
-
res.end(JSON.stringify({ entries: [],
|
|
926
|
+
res.end(JSON.stringify({ entries: [], actions: [], events: [], task: workerTask }));
|
|
589
927
|
return;
|
|
590
928
|
}
|
|
591
929
|
const content = await readFile(file, "utf-8");
|
|
592
|
-
|
|
930
|
+
const { entries, actions, events } = parseSessionTail(content, tail);
|
|
931
|
+
res.end(JSON.stringify({ entries, actions, events, task: workerTask }));
|
|
593
932
|
return;
|
|
594
933
|
}
|
|
595
934
|
res.writeHead(404);
|