@ryanfw/prompt-orchestration-pipeline 0.6.0 → 0.8.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 +1 -2
- package/package.json +1 -2
- package/src/api/validators/json.js +39 -0
- package/src/components/DAGGrid.jsx +392 -303
- package/src/components/JobCard.jsx +13 -11
- package/src/components/JobDetail.jsx +41 -71
- package/src/components/JobTable.jsx +32 -22
- package/src/components/Layout.jsx +0 -21
- package/src/components/LiveText.jsx +47 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/ui/RestartJobModal.jsx +140 -0
- package/src/components/ui/toast.jsx +138 -0
- package/src/config/models.js +322 -0
- package/src/config/statuses.js +119 -0
- package/src/core/config.js +2 -164
- package/src/core/file-io.js +1 -1
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +52 -26
- package/src/core/status-writer.js +147 -3
- package/src/core/symlink-bridge.js +55 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +267 -443
- package/src/llm/index.js +167 -52
- package/src/pages/Code.jsx +57 -3
- package/src/pages/PipelineDetail.jsx +92 -22
- package/src/pages/PromptPipelineDashboard.jsx +15 -36
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +17 -34
- package/src/providers/gemini.js +226 -0
- package/src/providers/openai.js +36 -106
- package/src/providers/zhipu.js +136 -0
- package/src/ui/client/adapters/job-adapter.js +16 -26
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
- package/src/ui/client/index.css +9 -0
- package/src/ui/client/index.html +1 -0
- package/src/ui/client/main.jsx +18 -15
- package/src/ui/client/time-store.js +161 -0
- package/src/ui/config-bridge.js +15 -24
- package/src/ui/config-bridge.node.js +15 -24
- package/src/ui/dist/assets/{index-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +3 -2
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +231 -38
- package/src/ui/transformers/status-transformer.js +18 -31
- package/src/ui/watcher.js +5 -1
- package/src/utils/dag.js +8 -4
- package/src/utils/duration.js +13 -19
- package/src/utils/formatters.js +27 -0
- package/src/utils/geometry-equality.js +83 -0
- package/src/utils/pipelines.js +5 -1
- package/src/utils/time-utils.js +40 -0
- package/src/utils/token-cost-calculator.js +4 -7
- package/src/utils/ui.jsx +14 -16
- package/src/components/ui/select.jsx +0 -27
- package/src/lib/utils.js +0 -6
- package/src/ui/client/hooks/useTicker.js +0 -26
- package/src/ui/config-bridge.browser.js +0 -149
- package/src/ui/dist/assets/style-x0V-5m8e.css +0 -62
package/src/ui/dist/index.html
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
rel="stylesheet"
|
|
11
11
|
/>
|
|
12
12
|
<title>Prompt Pipeline Dashboard</title>
|
|
13
|
-
<
|
|
14
|
-
<
|
|
13
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DqkbzXZ1.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/style-DBF9NQGk.css">
|
|
15
16
|
</head>
|
|
16
17
|
<body>
|
|
17
18
|
<div id="root"></div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="32"
|
|
4
|
+
height="32"
|
|
5
|
+
viewBox="0 0 1200 1200"
|
|
6
|
+
>
|
|
7
|
+
<path
|
|
8
|
+
fill="#009966"
|
|
9
|
+
d="M406.13 988.31c-17.297 75.047-84.562 131.11-164.86 131.11-93.375 0-169.18-75.797-169.18-169.18s75.797-169.18 169.18-169.18 169.18 75.797 169.18 169.18v1.266h447.74v-167.9H671.63c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.703-16.406-39.562v-37.312h-317.16c-10.312 0-18.656-8.344-18.656-18.656V355.78h-147.94c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.75-16.406-39.562v-111.94c0-14.859 5.906-29.109 16.406-39.562 10.5-10.5 24.75-16.406 39.562-16.406h391.78c14.859 0 29.062 5.906 39.562 16.406s16.406 24.75 16.406 39.562v37.312h202.4c9.281-84.609 81.094-150.52 168.14-150.52 93.375 0 169.18 75.797 169.18 169.18s-75.797 169.18-169.18 169.18c-87.047 0-158.86-65.906-168.14-150.52h-202.4v37.312c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-206.53v297.24h298.5v-37.312c0-14.859 5.906-29.062 16.406-39.562s24.703-16.406 39.562-16.406h392.63c14.859 0 29.062 5.906 39.562 16.406s16.406 24.703 16.406 39.562v111.94c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-168.74v186.56c0 10.312-8.344 18.656-18.656 18.656h-466.4c-1.5 0-2.906-.187-4.312-.516zM225.19 262.45h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H225.19c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H411.75c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm616.18 0h85.5c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-85.5l29.062-22.594c8.109-6.328 9.609-18.047 3.281-26.156s-18.047-9.609-26.156-3.281l-71.953 55.969a18.61 18.61 0 0 0 0 29.438l71.953 55.969c8.109 6.328 19.875 4.875 26.156-3.281 6.328-8.109 4.875-19.875-3.281-26.203l-29.062-22.594zm-779.95 696.66l50.391 50.391c7.266 7.313 19.078 7.313 26.391 0l100.73-100.73c7.266-7.266 7.266-19.078 0-26.391-7.266-7.266-19.078-7.266-26.391 0l-87.562 87.562-37.172-37.172c-7.266-7.266-19.078-7.266-26.391 0-7.266 7.266-7.266 19.078 0 26.391zm797.21-268.78h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H858.63c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656z"
|
|
10
|
+
fill-rule="evenodd"
|
|
11
|
+
/>
|
|
12
|
+
</svg>
|
package/src/ui/server.js
CHANGED
|
@@ -12,6 +12,8 @@ import * as state from "./state.js";
|
|
|
12
12
|
// Import orchestrator-related functions only in non-test mode
|
|
13
13
|
let submitJobWithValidation;
|
|
14
14
|
import { sseRegistry } from "./sse.js";
|
|
15
|
+
import { resetJobToCleanSlate } from "../core/status-writer.js";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
15
17
|
import {
|
|
16
18
|
getPendingSeedPath,
|
|
17
19
|
resolvePipelinePaths,
|
|
@@ -29,6 +31,22 @@ const __dirname = path.dirname(__filename);
|
|
|
29
31
|
// Vite dev server instance (populated in development mode)
|
|
30
32
|
let viteServer = null;
|
|
31
33
|
|
|
34
|
+
// In-memory restart guard to prevent duplicate concurrent restarts per job
|
|
35
|
+
const restartingJobs = new Set();
|
|
36
|
+
|
|
37
|
+
// Helper functions for restart guard
|
|
38
|
+
function isRestartInProgress(jobId) {
|
|
39
|
+
return restartingJobs.has(jobId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function beginRestart(jobId) {
|
|
43
|
+
restartingJobs.add(jobId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function endRestart(jobId) {
|
|
47
|
+
restartingJobs.delete(jobId);
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
// Configuration
|
|
33
51
|
const PORT = process.env.PORT || 4000;
|
|
34
52
|
const WATCHED_PATHS = (
|
|
@@ -1412,49 +1430,206 @@ function createServer() {
|
|
|
1412
1430
|
// Route: GET /api/llm/functions
|
|
1413
1431
|
if (pathname === "/api/llm/functions" && req.method === "GET") {
|
|
1414
1432
|
try {
|
|
1415
|
-
const {
|
|
1416
|
-
const config = getConfig();
|
|
1417
|
-
|
|
1418
|
-
// Helper to convert model alias to camelCase function name
|
|
1419
|
-
const toCamelCase = (alias) => {
|
|
1420
|
-
const [provider, ...modelParts] = alias.split(":");
|
|
1421
|
-
const model = modelParts.join("-");
|
|
1422
|
-
const camelModel = model.replace(/-([a-z0-9])/g, (match, char) =>
|
|
1423
|
-
char.toUpperCase()
|
|
1424
|
-
);
|
|
1425
|
-
return camelModel;
|
|
1426
|
-
};
|
|
1433
|
+
const { PROVIDER_FUNCTIONS } = await import("../config/models.js");
|
|
1427
1434
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1435
|
+
sendJson(res, 200, PROVIDER_FUNCTIONS);
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
console.error("Error handling /api/llm/functions:", error);
|
|
1438
|
+
sendJson(res, 500, {
|
|
1439
|
+
ok: false,
|
|
1440
|
+
error: "internal_error",
|
|
1441
|
+
message: "Failed to get LLM functions",
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1431
1446
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1447
|
+
// Route: POST /api/jobs/:jobId/restart
|
|
1448
|
+
if (
|
|
1449
|
+
pathname.startsWith("/api/jobs/") &&
|
|
1450
|
+
pathname.endsWith("/restart") &&
|
|
1451
|
+
req.method === "POST"
|
|
1452
|
+
) {
|
|
1453
|
+
const pathMatch = pathname.match(/^\/api\/jobs\/([^\/]+)\/restart$/);
|
|
1454
|
+
if (!pathMatch) {
|
|
1455
|
+
sendJson(res, 400, {
|
|
1456
|
+
ok: false,
|
|
1457
|
+
error: "bad_request",
|
|
1458
|
+
message: "Invalid path format",
|
|
1459
|
+
});
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const [, jobId] = pathMatch;
|
|
1464
|
+
const dataDir = process.env.PO_ROOT || DATA_DIR;
|
|
1465
|
+
|
|
1466
|
+
try {
|
|
1467
|
+
// Validate jobId
|
|
1468
|
+
if (!jobId || typeof jobId !== "string" || jobId.trim() === "") {
|
|
1469
|
+
sendJson(res, 400, {
|
|
1470
|
+
ok: false,
|
|
1471
|
+
error: "bad_request",
|
|
1472
|
+
message: "jobId is required",
|
|
1473
|
+
});
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Resolve job lifecycle
|
|
1478
|
+
const lifecycle = await resolveJobLifecycle(dataDir, jobId);
|
|
1479
|
+
if (!lifecycle) {
|
|
1480
|
+
sendJson(res, 404, {
|
|
1481
|
+
ok: false,
|
|
1482
|
+
code: "job_not_found",
|
|
1483
|
+
message: "Job not found",
|
|
1484
|
+
});
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1435
1487
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1488
|
+
// Only support current lifecycle for MVP
|
|
1489
|
+
if (lifecycle !== "current") {
|
|
1490
|
+
sendJson(res, 409, {
|
|
1491
|
+
ok: false,
|
|
1492
|
+
code: "unsupported_lifecycle",
|
|
1493
|
+
message:
|
|
1494
|
+
"Job restart is only supported for jobs in 'current' lifecycle",
|
|
1495
|
+
});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Check if job is already running
|
|
1500
|
+
const jobDir = getJobDirectoryPath(dataDir, jobId, "current");
|
|
1501
|
+
const statusPath = path.join(jobDir, "tasks-status.json");
|
|
1502
|
+
|
|
1503
|
+
let snapshot;
|
|
1504
|
+
try {
|
|
1505
|
+
const content = await fs.promises.readFile(statusPath, "utf8");
|
|
1506
|
+
snapshot = JSON.parse(content);
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
if (error.code === "ENOENT") {
|
|
1509
|
+
sendJson(res, 404, {
|
|
1510
|
+
ok: false,
|
|
1511
|
+
code: "job_not_found",
|
|
1512
|
+
message: "Job status file not found",
|
|
1513
|
+
});
|
|
1514
|
+
return;
|
|
1438
1515
|
}
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Guard against running jobs
|
|
1520
|
+
if (snapshot.state === "running") {
|
|
1521
|
+
sendJson(res, 409, {
|
|
1522
|
+
ok: false,
|
|
1523
|
+
code: "job_running",
|
|
1524
|
+
message: "Job is currently running",
|
|
1525
|
+
});
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1439
1528
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1529
|
+
// Guard against concurrent restarts
|
|
1530
|
+
if (isRestartInProgress(jobId)) {
|
|
1531
|
+
sendJson(res, 409, {
|
|
1532
|
+
ok: false,
|
|
1533
|
+
code: "job_running",
|
|
1534
|
+
message: "Job restart is already in progress",
|
|
1446
1535
|
});
|
|
1536
|
+
return;
|
|
1447
1537
|
}
|
|
1448
1538
|
|
|
1449
|
-
|
|
1539
|
+
// Begin restart guard
|
|
1540
|
+
beginRestart(jobId);
|
|
1541
|
+
|
|
1542
|
+
try {
|
|
1543
|
+
// Parse optional fromTask from request body for targeted restart
|
|
1544
|
+
let body = {};
|
|
1545
|
+
try {
|
|
1546
|
+
const rawBody = await readRawBody(req);
|
|
1547
|
+
body = JSON.parse(rawBody.toString("utf8"));
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
sendJson(res, 400, {
|
|
1550
|
+
ok: false,
|
|
1551
|
+
error: "bad_request",
|
|
1552
|
+
message: "Invalid JSON in request body",
|
|
1553
|
+
});
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const { fromTask } = body;
|
|
1558
|
+
|
|
1559
|
+
// Reset job: clean-slate or partial from a specific task
|
|
1560
|
+
const { resetJobFromTask } = await import("../core/status-writer.js");
|
|
1561
|
+
if (fromTask) {
|
|
1562
|
+
await resetJobFromTask(jobDir, fromTask, { clearTokenUsage: true });
|
|
1563
|
+
} else {
|
|
1564
|
+
await resetJobToCleanSlate(jobDir, { clearTokenUsage: true });
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// Spawn detached pipeline-runner process
|
|
1568
|
+
const runnerPath = path.join(__dirname, "../core/pipeline-runner.js");
|
|
1569
|
+
const base = process.env.PO_ROOT || DATA_DIR;
|
|
1570
|
+
const env = {
|
|
1571
|
+
...process.env,
|
|
1572
|
+
PO_ROOT: base,
|
|
1573
|
+
PO_DATA_DIR: path.join(base, "pipeline-data"),
|
|
1574
|
+
PO_PENDING_DIR: path.join(base, "pipeline-data", "pending"),
|
|
1575
|
+
PO_CURRENT_DIR: path.join(base, "pipeline-data", "current"),
|
|
1576
|
+
PO_COMPLETE_DIR: path.join(base, "pipeline-data", "complete"),
|
|
1577
|
+
...(fromTask && { PO_START_FROM_TASK: fromTask }),
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
const child = spawn(process.execPath, [runnerPath, jobId], {
|
|
1581
|
+
env,
|
|
1582
|
+
stdio: "ignore",
|
|
1583
|
+
detached: true,
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
// Unref the child process so it runs in the background
|
|
1587
|
+
child.unref();
|
|
1588
|
+
|
|
1589
|
+
// Send success response
|
|
1590
|
+
sendJson(res, 202, {
|
|
1591
|
+
ok: true,
|
|
1592
|
+
jobId,
|
|
1593
|
+
mode: "clean-slate",
|
|
1594
|
+
spawned: true,
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
console.log(
|
|
1598
|
+
`Job ${jobId} restarted successfully, detached runner PID: ${child.pid}`
|
|
1599
|
+
);
|
|
1600
|
+
} finally {
|
|
1601
|
+
// Always end restart guard
|
|
1602
|
+
endRestart(jobId);
|
|
1603
|
+
}
|
|
1450
1604
|
} catch (error) {
|
|
1451
|
-
console.error(
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
}
|
|
1605
|
+
console.error(`Error handling POST /api/jobs/${jobId}/restart:`, error);
|
|
1606
|
+
|
|
1607
|
+
// Clean up restart guard on error
|
|
1608
|
+
if (isRestartInProgress(jobId)) {
|
|
1609
|
+
endRestart(jobId);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
if (error.code === "ENOENT") {
|
|
1613
|
+
sendJson(res, 404, {
|
|
1614
|
+
ok: false,
|
|
1615
|
+
code: "job_not_found",
|
|
1616
|
+
message: "Job directory not found",
|
|
1617
|
+
});
|
|
1618
|
+
} else if (error.code === "spawn failed") {
|
|
1619
|
+
sendJson(res, 500, {
|
|
1620
|
+
ok: false,
|
|
1621
|
+
code: "spawn_failed",
|
|
1622
|
+
message: error.message || "Failed to spawn pipeline runner",
|
|
1623
|
+
});
|
|
1624
|
+
} else {
|
|
1625
|
+
sendJson(res, 500, {
|
|
1626
|
+
ok: false,
|
|
1627
|
+
code: "internal_error",
|
|
1628
|
+
message: "Internal server error",
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1457
1631
|
}
|
|
1632
|
+
|
|
1458
1633
|
return;
|
|
1459
1634
|
}
|
|
1460
1635
|
|
|
@@ -1490,6 +1665,25 @@ function createServer() {
|
|
|
1490
1665
|
return;
|
|
1491
1666
|
}
|
|
1492
1667
|
|
|
1668
|
+
// Route: GET /favicon.svg
|
|
1669
|
+
if (pathname === "/favicon.svg" && req.method === "GET") {
|
|
1670
|
+
const faviconPath = path.join(__dirname, "public", "favicon.svg");
|
|
1671
|
+
|
|
1672
|
+
try {
|
|
1673
|
+
const content = await fs.promises.readFile(faviconPath, "utf8");
|
|
1674
|
+
res.writeHead(200, {
|
|
1675
|
+
"Content-Type": "image/svg+xml",
|
|
1676
|
+
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
|
|
1677
|
+
});
|
|
1678
|
+
res.end(content);
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
console.error("Error serving favicon:", error);
|
|
1681
|
+
res.writeHead(404);
|
|
1682
|
+
res.end("Favicon not found");
|
|
1683
|
+
}
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1493
1687
|
// Unknown API endpoint fallback (keep API responses in JSON)
|
|
1494
1688
|
if (pathname.startsWith("/api/")) {
|
|
1495
1689
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1678,11 +1872,6 @@ async function startServer({ dataDir, port: customPort }) {
|
|
|
1678
1872
|
const { initPATHS } = await import("./config-bridge.node.js");
|
|
1679
1873
|
initPATHS(dataDir);
|
|
1680
1874
|
|
|
1681
|
-
// Set the data directory environment variable
|
|
1682
|
-
if (dataDir) {
|
|
1683
|
-
process.env.PO_ROOT = dataDir;
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
1875
|
// Require PO_ROOT for non-test runs
|
|
1687
1876
|
if (!process.env.PO_ROOT) {
|
|
1688
1877
|
if (process.env.NODE_ENV !== "test") {
|
|
@@ -1853,6 +2042,10 @@ export {
|
|
|
1853
2042
|
initializeWatcher,
|
|
1854
2043
|
state,
|
|
1855
2044
|
resolveJobLifecycle,
|
|
2045
|
+
restartingJobs,
|
|
2046
|
+
isRestartInProgress,
|
|
2047
|
+
beginRestart,
|
|
2048
|
+
endRestart,
|
|
1856
2049
|
};
|
|
1857
2050
|
|
|
1858
2051
|
// Start server if run directly
|
|
@@ -4,24 +4,12 @@ import {
|
|
|
4
4
|
calculateJobCosts,
|
|
5
5
|
formatCostDataForAPI,
|
|
6
6
|
} from "../../utils/token-cost-calculator.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export function determineJobStatus(tasks = {}) {
|
|
14
|
-
if (!tasks || typeof tasks !== "object") return "pending";
|
|
15
|
-
const names = Object.keys(tasks);
|
|
16
|
-
if (names.length === 0) return "pending";
|
|
17
|
-
|
|
18
|
-
const states = names.map((n) => tasks[n]?.state);
|
|
19
|
-
|
|
20
|
-
if (states.includes("failed")) return "failed";
|
|
21
|
-
if (states.includes("running")) return "running";
|
|
22
|
-
if (states.every((s) => s === "done")) return "complete";
|
|
23
|
-
return "pending";
|
|
24
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
VALID_TASK_STATES,
|
|
9
|
+
normalizeTaskState,
|
|
10
|
+
deriveJobStatusFromTasks,
|
|
11
|
+
TaskState,
|
|
12
|
+
} from "../../config/statuses.js";
|
|
25
13
|
|
|
26
14
|
/**
|
|
27
15
|
* Compute job status object { status, progress } and emit warnings for unknown states.
|
|
@@ -50,14 +38,14 @@ export function computeJobStatus(tasksInput, existingProgress = null) {
|
|
|
50
38
|
const t = tasksInput[name];
|
|
51
39
|
const state = t && typeof t === "object" ? t.state : undefined;
|
|
52
40
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} else {
|
|
59
|
-
normalized[name] = { state };
|
|
41
|
+
const normalizedState = normalizeTaskState(state);
|
|
42
|
+
|
|
43
|
+
// Track unknown states for warning
|
|
44
|
+
if (state != null && state !== normalizedState) {
|
|
45
|
+
unknownStatesFound.add(state);
|
|
60
46
|
}
|
|
47
|
+
|
|
48
|
+
normalized[name] = { state: normalizedState };
|
|
61
49
|
}
|
|
62
50
|
|
|
63
51
|
// Warn for unknown states
|
|
@@ -65,7 +53,7 @@ export function computeJobStatus(tasksInput, existingProgress = null) {
|
|
|
65
53
|
console.warn(`Unknown task state "${s}"`);
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
const status =
|
|
56
|
+
const status = deriveJobStatusFromTasks(Object.values(normalized));
|
|
69
57
|
// Use existing progress if provided, otherwise default to 0
|
|
70
58
|
// Progress is pre-calculated in task-statuses.json, not computed from task states
|
|
71
59
|
const progress = existingProgress !== null ? existingProgress : 0;
|
|
@@ -103,12 +91,11 @@ export function transformTasks(rawTasks) {
|
|
|
103
91
|
const rawState =
|
|
104
92
|
raw && typeof raw === "object" && "state" in raw ? raw.state : undefined;
|
|
105
93
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
const finalState = normalizeTaskState(rawState);
|
|
95
|
+
|
|
96
|
+
// Warn for invalid states (different from normalized)
|
|
97
|
+
if (rawState != null && rawState !== finalState) {
|
|
110
98
|
console.warn(`Invalid task state "${rawState}"`);
|
|
111
|
-
finalState = "pending";
|
|
112
99
|
}
|
|
113
100
|
|
|
114
101
|
const task = {
|
package/src/ui/watcher.js
CHANGED
|
@@ -37,7 +37,11 @@ export function start(paths, onChange, options = {}) {
|
|
|
37
37
|
|
|
38
38
|
// Initialize chokidar watcher
|
|
39
39
|
const watcher = chokidar.watch(paths, {
|
|
40
|
-
ignored:
|
|
40
|
+
ignored: [
|
|
41
|
+
/(^|[\/\\])(\.git|node_modules|dist)([\/\\]|$)/,
|
|
42
|
+
/pipeline-data\/[^/]+\/[^/]+\/tasks\/[^/]+\/_task_root([\/\\]|$)/,
|
|
43
|
+
],
|
|
44
|
+
followSymlinks: false,
|
|
41
45
|
persistent: true,
|
|
42
46
|
ignoreInitial: true,
|
|
43
47
|
});
|
package/src/utils/dag.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TaskState } from "../config/statuses.js";
|
|
2
|
+
|
|
1
3
|
function normalizeJobTasks(tasks) {
|
|
2
4
|
if (!tasks) return {};
|
|
3
5
|
|
|
@@ -53,7 +55,7 @@ export function computeDagItems(job, pipeline) {
|
|
|
53
55
|
const jobTask = jobTasks[taskId];
|
|
54
56
|
return {
|
|
55
57
|
id: taskId,
|
|
56
|
-
status: jobTask ? jobTask.state :
|
|
58
|
+
status: jobTask ? jobTask.state : TaskState.PENDING,
|
|
57
59
|
source: "pipeline",
|
|
58
60
|
stage: computeTaskStage(job, taskId),
|
|
59
61
|
};
|
|
@@ -82,18 +84,20 @@ export function computeActiveIndex(items) {
|
|
|
82
84
|
|
|
83
85
|
// Find first running task
|
|
84
86
|
const firstRunningIndex = items.findIndex(
|
|
85
|
-
(item) => item.status ===
|
|
87
|
+
(item) => item.status === TaskState.RUNNING
|
|
86
88
|
);
|
|
87
89
|
if (firstRunningIndex !== -1) return firstRunningIndex;
|
|
88
90
|
|
|
89
91
|
// Find first failed task
|
|
90
|
-
const firstFailedIndex = items.findIndex(
|
|
92
|
+
const firstFailedIndex = items.findIndex(
|
|
93
|
+
(item) => item.status === TaskState.FAILED
|
|
94
|
+
);
|
|
91
95
|
if (firstFailedIndex !== -1) return firstFailedIndex;
|
|
92
96
|
|
|
93
97
|
// Find last completed task
|
|
94
98
|
let lastDoneIndex = -1;
|
|
95
99
|
items.forEach((item, index) => {
|
|
96
|
-
if (item.status ===
|
|
100
|
+
if (item.status === TaskState.DONE) lastDoneIndex = index;
|
|
97
101
|
});
|
|
98
102
|
|
|
99
103
|
if (lastDoneIndex !== -1) return lastDoneIndex;
|
package/src/utils/duration.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Duration policy utilities for consistent time display across components
|
|
3
3
|
*/
|
|
4
|
+
import { TaskState, normalizeTaskState } from "../config/statuses.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Normalizes task state names to canonical values
|
|
@@ -8,21 +9,15 @@
|
|
|
8
9
|
* @returns {string} Normalized state
|
|
9
10
|
*/
|
|
10
11
|
export function normalizeState(state) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case "pending":
|
|
18
|
-
case "running":
|
|
19
|
-
case "current":
|
|
20
|
-
case "completed":
|
|
21
|
-
case "rejected":
|
|
22
|
-
return state;
|
|
23
|
-
default:
|
|
24
|
-
return state; // Pass through unknown states
|
|
12
|
+
// Use centralized normalization, then map to duration-specific canonical forms
|
|
13
|
+
const canonicalState = normalizeTaskState(state);
|
|
14
|
+
|
|
15
|
+
// Duration utilities use "completed" instead of "done" for legacy compatibility
|
|
16
|
+
if (canonicalState === TaskState.DONE) {
|
|
17
|
+
return "completed";
|
|
25
18
|
}
|
|
19
|
+
|
|
20
|
+
return canonicalState;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
/**
|
|
@@ -36,18 +31,17 @@ export function taskDisplayDurationMs(task, now = Date.now()) {
|
|
|
36
31
|
const normalizedState = normalizeState(state);
|
|
37
32
|
|
|
38
33
|
switch (normalizedState) {
|
|
39
|
-
case
|
|
34
|
+
case TaskState.PENDING:
|
|
40
35
|
return 0;
|
|
41
36
|
|
|
42
|
-
case
|
|
43
|
-
case "current":
|
|
37
|
+
case TaskState.RUNNING:
|
|
44
38
|
if (!startedAt) {
|
|
45
39
|
return 0;
|
|
46
40
|
}
|
|
47
41
|
const startTime = Date.parse(startedAt);
|
|
48
42
|
return Math.max(0, now - startTime);
|
|
49
43
|
|
|
50
|
-
case "completed":
|
|
44
|
+
case "completed": // Duration utilities still use "completed" for legacy compatibility
|
|
51
45
|
// Prefer executionTimeMs or executionTime if available, even without startedAt
|
|
52
46
|
const execTime =
|
|
53
47
|
executionTimeMs != null ? executionTimeMs : executionTime;
|
|
@@ -63,7 +57,7 @@ export function taskDisplayDurationMs(task, now = Date.now()) {
|
|
|
63
57
|
const endTime = endedAt ? Date.parse(endedAt) : now;
|
|
64
58
|
return Math.max(0, endTime - completedStartTime);
|
|
65
59
|
|
|
66
|
-
case
|
|
60
|
+
case TaskState.FAILED:
|
|
67
61
|
return 0;
|
|
68
62
|
|
|
69
63
|
default:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format currency with 4 decimal places, trimming trailing zeros
|
|
3
|
+
* @param {number} x - The number to format
|
|
4
|
+
* @returns {string} Formatted currency string
|
|
5
|
+
*/
|
|
6
|
+
export function formatCurrency4(x) {
|
|
7
|
+
if (typeof x !== "number" || x === 0) return "$0.0000";
|
|
8
|
+
const formatted = x.toFixed(4);
|
|
9
|
+
// Trim trailing zeros and unnecessary decimal point
|
|
10
|
+
return `$${formatted.replace(/\.?0+$/, "")}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format tokens in compact notation (k, M suffixes)
|
|
15
|
+
* @param {number} n - The number of tokens to format
|
|
16
|
+
* @returns {string} Formatted tokens string
|
|
17
|
+
*/
|
|
18
|
+
export function formatTokensCompact(n) {
|
|
19
|
+
if (typeof n !== "number" || n === 0) return "0 tok";
|
|
20
|
+
|
|
21
|
+
if (n >= 1000000) {
|
|
22
|
+
return `${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M tokens`;
|
|
23
|
+
} else if (n >= 1000) {
|
|
24
|
+
return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k tokens`;
|
|
25
|
+
}
|
|
26
|
+
return `${n} tokens`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two geometry snapshots for layout-relevant changes using tolerance.
|
|
3
|
+
* @param {Object} prev - Previous geometry snapshot
|
|
4
|
+
* @param {Object} next - New geometry snapshot
|
|
5
|
+
* @param {number} epsilon - Tolerance in pixels for floating-point differences (default: 0.5)
|
|
6
|
+
* @returns {boolean} true if geometries are effectively equal for rendering purposes
|
|
7
|
+
*/
|
|
8
|
+
export function areGeometriesEqual(prev, next, epsilon = 0.5) {
|
|
9
|
+
// Strict equality shortcut
|
|
10
|
+
if (prev === next) return true;
|
|
11
|
+
if (!prev || !next) return false;
|
|
12
|
+
|
|
13
|
+
// Compare top-level scalars
|
|
14
|
+
if (prev.itemsLength !== next.itemsLength) return false;
|
|
15
|
+
if (prev.effectiveCols !== next.effectiveCols) return false;
|
|
16
|
+
|
|
17
|
+
// Compare overlay box numeric fields only (DOMRect may have non-numeric props)
|
|
18
|
+
if (!areOverlayBoxesEqual(prev.overlayBox, next.overlayBox, epsilon)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Compare boxes array length and each box's layout-relevant fields
|
|
23
|
+
const prevBoxes = prev.boxes;
|
|
24
|
+
const nextBoxes = next.boxes;
|
|
25
|
+
if (prevBoxes.length !== nextBoxes.length) return false;
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < prevBoxes.length; i++) {
|
|
28
|
+
if (!areBoxesEqual(prevBoxes[i], nextBoxes[i], epsilon)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare overlay box numeric fields with tolerance.
|
|
38
|
+
* @param {DOMRect|Object} a
|
|
39
|
+
* @param {DOMRect|Object} b
|
|
40
|
+
* @param {number} epsilon
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function areOverlayBoxesEqual(a, b, epsilon) {
|
|
44
|
+
return (
|
|
45
|
+
areNumbersClose(a.left, b.left, epsilon) &&
|
|
46
|
+
areNumbersClose(a.top, b.top, epsilon) &&
|
|
47
|
+
areNumbersClose(a.width, b.width, epsilon) &&
|
|
48
|
+
areNumbersClose(a.height, b.height, epsilon) &&
|
|
49
|
+
areNumbersClose(a.right, b.right, epsilon) &&
|
|
50
|
+
areNumbersClose(a.bottom, b.bottom, epsilon)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare individual card box layout fields with tolerance.
|
|
56
|
+
* @param {Object} a - box object with left/top/width/height/right/bottom/headerMidY
|
|
57
|
+
* @param {Object} b - box object with same shape
|
|
58
|
+
* @param {number} epsilon
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function areBoxesEqual(a, b, epsilon) {
|
|
62
|
+
if (!a || !b) return a === b;
|
|
63
|
+
return (
|
|
64
|
+
areNumbersClose(a.left, b.left, epsilon) &&
|
|
65
|
+
areNumbersClose(a.top, b.top, epsilon) &&
|
|
66
|
+
areNumbersClose(a.width, b.width, epsilon) &&
|
|
67
|
+
areNumbersClose(a.height, b.height, epsilon) &&
|
|
68
|
+
areNumbersClose(a.right, b.right, epsilon) &&
|
|
69
|
+
areNumbersClose(a.bottom, b.bottom, epsilon) &&
|
|
70
|
+
areNumbersClose(a.headerMidY, b.headerMidY, epsilon)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Numeric comparison with tolerance.
|
|
76
|
+
* @param {number} a
|
|
77
|
+
* @param {number} b
|
|
78
|
+
* @param {number} epsilon
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
function areNumbersClose(a, b, epsilon) {
|
|
82
|
+
return Math.abs(a - b) <= epsilon;
|
|
83
|
+
}
|
package/src/utils/pipelines.js
CHANGED
|
@@ -33,8 +33,12 @@ export function derivePipelineMetadata(source = {}) {
|
|
|
33
33
|
? pipelineSlugFromSource
|
|
34
34
|
: null);
|
|
35
35
|
|
|
36
|
+
// Also return string pipeline value directly if it's a string
|
|
37
|
+
const stringPipeline =
|
|
38
|
+
typeof pipelineValue === "string" ? pipelineValue : null;
|
|
39
|
+
|
|
36
40
|
return {
|
|
37
|
-
pipeline,
|
|
41
|
+
pipeline: pipeline || stringPipeline,
|
|
38
42
|
pipelineSlug:
|
|
39
43
|
typeof pipelineSlugFromSource === "string"
|
|
40
44
|
? pipelineSlugFromSource
|