bosun 0.40.21 → 0.41.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/agent/agent-custom-tools.mjs +23 -5
- package/agent/agent-pool.mjs +6 -2
- package/agent/primary-agent.mjs +81 -7
- package/bench/swebench/bosun-swebench.mjs +5 -0
- package/cli.mjs +208 -3
- package/config/config-doctor.mjs +51 -2
- package/config/config.mjs +103 -3
- package/github/github-auth-manager.mjs +70 -19
- package/infra/library-manager.mjs +894 -60
- package/infra/monitor.mjs +8 -2
- package/infra/session-tracker.mjs +13 -3
- package/infra/test-runtime.mjs +267 -0
- package/package.json +8 -5
- package/server/setup-web-server.mjs +4 -1
- package/server/ui-server.mjs +1323 -20
- package/task/task-claims.mjs +6 -10
- package/ui/components/chat-view.js +18 -1
- package/ui/components/workspace-switcher.js +321 -9
- package/ui/demo-defaults.js +11746 -9470
- package/ui/demo.html +9 -1
- package/ui/modules/router.js +1 -1
- package/ui/modules/voice-client-sdk.js +1 -1
- package/ui/modules/voice-client.js +33 -2
- package/ui/styles/components.css +514 -1
- package/ui/tabs/library.js +410 -55
- package/ui/tabs/tasks.js +1052 -506
- package/ui/tabs/workflow-canvas-utils.mjs +30 -0
- package/ui/tabs/workflows.js +914 -298
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +24 -16
- package/workflow/project-detection.mjs +559 -0
- package/workflow/workflow-contract.mjs +433 -232
- package/workflow/workflow-engine.mjs +181 -30
- package/workflow/workflow-nodes.mjs +304 -6
- package/workflow/workflow-templates.mjs +92 -16
- package/workflow-templates/agents.mjs +20 -19
- package/workflow-templates/code-quality.mjs +20 -14
- package/workflow-templates/task-batch.mjs +3 -2
- package/workflow-templates/task-execution.mjs +752 -0
- package/workflow-templates/task-lifecycle.mjs +34 -8
- package/workspace/workspace-manager.mjs +151 -0
package/ui/tabs/workflows.js
CHANGED
|
@@ -10,13 +10,14 @@ import htm from "htm";
|
|
|
10
10
|
const html = htm.bind(h);
|
|
11
11
|
|
|
12
12
|
import { haptic } from "../modules/telegram.js";
|
|
13
|
-
import { apiFetch } from "../modules/api.js";
|
|
13
|
+
import { apiFetch, onWsMessage } from "../modules/api.js";
|
|
14
14
|
import { showToast, refreshTab } from "../modules/state.js";
|
|
15
15
|
import { navigateTo, routeParams, setRouteParams } from "../modules/router.js";
|
|
16
16
|
import { ICONS } from "../modules/icons.js";
|
|
17
17
|
import { resolveIcon } from "../modules/icon-utils.js";
|
|
18
18
|
import { formatDate, formatDuration, formatRelative } from "../modules/utils.js";
|
|
19
19
|
import {
|
|
20
|
+
buildNodeStatusesFromRunDetail,
|
|
20
21
|
createHistoryState,
|
|
21
22
|
getNodeSearchMetadata,
|
|
22
23
|
parseGraphSnapshot,
|
|
@@ -60,6 +61,11 @@ const connectingFrom = signal(null);
|
|
|
60
61
|
const viewMode = signal("list"); // "list" | "canvas" | "runs"
|
|
61
62
|
const WORKFLOW_RUN_PAGE_SIZE = 50;
|
|
62
63
|
const WORKFLOW_RUN_MAX_FETCH = 5000;
|
|
64
|
+
const WORKFLOW_LIVE_POLL_MS = 3000;
|
|
65
|
+
const WORKFLOW_LIVE_WS_BATCH_MS = 90;
|
|
66
|
+
const NODE_COMPLETION_FLASH_MS = 1400;
|
|
67
|
+
const NODE_RUNNING_HINT_MS = 500;
|
|
68
|
+
const EDGE_FLOW_ANIMATION_MS = 1200;
|
|
63
69
|
const workflowRunsLimit = signal(WORKFLOW_RUN_PAGE_SIZE);
|
|
64
70
|
|
|
65
71
|
// ── Execute Dialog state ──────────────────────────────────────────────────
|
|
@@ -309,6 +315,11 @@ function humanizeVarKey(key) {
|
|
|
309
315
|
/** Infer a short helper text for a variable key */
|
|
310
316
|
function inferVarHelp(key, value) {
|
|
311
317
|
const k = key.toLowerCase();
|
|
318
|
+
if (k.includes("testcommand") || k.includes("test_command") || k === "testframework" || k === "test_framework") return "Test command for your project — select from presets or enter custom";
|
|
319
|
+
if (k.includes("buildcommand") || k.includes("build_command")) return "Build command for your project — select from presets or enter custom";
|
|
320
|
+
if (k.includes("lintcommand") || k.includes("lint_command") || k.includes("lintcmd")) return "Lint/style check command — select from presets or enter custom";
|
|
321
|
+
if (k.includes("syntaxcheck") || k.includes("syntax_check")) return "Syntax/compile check command — select from presets or enter custom";
|
|
322
|
+
if (k === "basebranch" || k === "base_branch" || k === "defaultbasebranch") return "Base branch for PRs — select from common options or enter custom";
|
|
312
323
|
if (k.includes("timeout") || k.includes("delay") || k.includes("cooldown")) return "Duration in milliseconds";
|
|
313
324
|
if (k.includes("branch")) return "Git branch name";
|
|
314
325
|
if (k.includes("url") || k.includes("endpoint")) return "URL / endpoint";
|
|
@@ -324,12 +335,24 @@ function inferVarHelp(key, value) {
|
|
|
324
335
|
function inferVarOptions(key, value) {
|
|
325
336
|
const k = String(key || "").toLowerCase();
|
|
326
337
|
const options = [];
|
|
338
|
+
|
|
327
339
|
if (k.includes("executor") || k.includes("sdk")) {
|
|
328
340
|
options.push("auto", "codex", "claude", "copilot");
|
|
329
341
|
} else if (k.includes("bumptype") || k.includes("bump_type")) {
|
|
330
342
|
options.push("patch", "minor", "major");
|
|
343
|
+
} else if (k.includes("testcommand") || k.includes("test_command") || k === "testframework" || k === "test_framework") {
|
|
344
|
+
options.push("npm test", "yarn test", "pnpm test", "pytest", "poetry run pytest", "go test ./...", "cargo test", "mvn test", "./gradlew test", "dotnet test", "bundle exec rspec", "make test");
|
|
345
|
+
} else if (k.includes("buildcommand") || k.includes("build_command")) {
|
|
346
|
+
options.push("npm run build", "yarn build", "pnpm build", "go build ./...", "cargo build", "mvn package -DskipTests", "./gradlew build", "dotnet build", "python -m build", "make");
|
|
347
|
+
} else if (k.includes("lintcommand") || k.includes("lint_command") || k.includes("lintcmd")) {
|
|
348
|
+
options.push("npm run lint", "npx eslint .", "ruff check .", "golangci-lint run", "cargo clippy -- -D warnings", "dotnet format --verify-no-changes", "bundle exec rubocop");
|
|
349
|
+
} else if (k.includes("syntaxcheck") || k.includes("syntax_check")) {
|
|
350
|
+
options.push("node --check", "npx tsc --noEmit", "python -m py_compile", "go vet ./...", "cargo check", "dotnet build --no-restore");
|
|
351
|
+
} else if (k === "basebranch" || k === "base_branch" || k === "defaultbasebranch" || k === "targetbranch") {
|
|
352
|
+
options.push("main", "master", "develop", "staging");
|
|
331
353
|
}
|
|
332
|
-
|
|
354
|
+
|
|
355
|
+
// Keep typed value when this field has known preset options.
|
|
333
356
|
if (options.length > 0 && typeof value === "string" && value.trim()) {
|
|
334
357
|
options.unshift(value.trim());
|
|
335
358
|
}
|
|
@@ -378,7 +401,12 @@ function isQuickVarKey(key) {
|
|
|
378
401
|
k.includes("sdk") ||
|
|
379
402
|
k.includes("model") ||
|
|
380
403
|
k.includes("branch") ||
|
|
381
|
-
k.includes("title")
|
|
404
|
+
k.includes("title") ||
|
|
405
|
+
k.includes("testcommand") || k.includes("test_command") ||
|
|
406
|
+
k === "testframework" || k === "test_framework" ||
|
|
407
|
+
k.includes("buildcommand") || k.includes("build_command") ||
|
|
408
|
+
k.includes("lintcommand") || k.includes("lint_command") ||
|
|
409
|
+
k.includes("syntaxcheck") || k.includes("syntax_check")
|
|
382
410
|
);
|
|
383
411
|
}
|
|
384
412
|
|
|
@@ -1207,6 +1235,109 @@ function stripEmoji(text) {
|
|
|
1207
1235
|
.trim();
|
|
1208
1236
|
}
|
|
1209
1237
|
|
|
1238
|
+
function normalizeLiveNodeStatus(status) {
|
|
1239
|
+
const normalized = String(status || "").trim().toLowerCase();
|
|
1240
|
+
if (normalized === "completed" || normalized === "success") return "success";
|
|
1241
|
+
if (normalized === "failed" || normalized === "fail" || normalized === "error") return "fail";
|
|
1242
|
+
if (normalized === "skipped" || normalized === "skip") return "skipped";
|
|
1243
|
+
if (normalized === "running") return "running";
|
|
1244
|
+
if (normalized === "waiting" || normalized === "pending") return "waiting";
|
|
1245
|
+
return normalized || "";
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function toNodeStatusLabel(status) {
|
|
1249
|
+
if (status === "success") return "success";
|
|
1250
|
+
if (status === "fail") return "fail";
|
|
1251
|
+
if (status === "skipped") return "skipped";
|
|
1252
|
+
return status || "";
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function getCanvasNodeExecutionVisuals(status, isSelected, selectedColor, flashState = "") {
|
|
1256
|
+
if (status === "running") {
|
|
1257
|
+
return {
|
|
1258
|
+
fill: "#10233f",
|
|
1259
|
+
stroke: "#60a5fa",
|
|
1260
|
+
strokeWidth: 2.5,
|
|
1261
|
+
filter: "url(#node-glow)",
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
if (status === "fail" || flashState === "fail") {
|
|
1265
|
+
return {
|
|
1266
|
+
fill: "#2a1217",
|
|
1267
|
+
stroke: "#ef4444",
|
|
1268
|
+
strokeWidth: 2.25,
|
|
1269
|
+
filter: "url(#node-shadow)",
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
if (status === "success" || flashState === "success") {
|
|
1273
|
+
return {
|
|
1274
|
+
fill: "#0f2a23",
|
|
1275
|
+
stroke: "#10b981",
|
|
1276
|
+
strokeWidth: 2,
|
|
1277
|
+
filter: "url(#node-shadow)",
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
if (status === "skipped" || flashState === "skipped") {
|
|
1281
|
+
return {
|
|
1282
|
+
fill: "#1f2430",
|
|
1283
|
+
stroke: "#94a3b8",
|
|
1284
|
+
strokeWidth: 2,
|
|
1285
|
+
filter: "url(#node-shadow)",
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (status === "waiting" || status === "pending") {
|
|
1289
|
+
return {
|
|
1290
|
+
fill: "#2f2310",
|
|
1291
|
+
stroke: "#f59e0b",
|
|
1292
|
+
strokeWidth: 2,
|
|
1293
|
+
filter: "url(#node-shadow)",
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
fill: isSelected ? "#1e293b" : "#1a1f2e",
|
|
1298
|
+
stroke: isSelected ? selectedColor : "#2a3040",
|
|
1299
|
+
strokeWidth: isSelected ? 2 : 1,
|
|
1300
|
+
filter: isSelected ? "url(#node-glow)" : "url(#node-shadow)",
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function resolveNodeOutputPreview(nodeType, preview = {}, fallbackOutput = null) {
|
|
1305
|
+
const lines = Array.isArray(preview?.lines) ? preview.lines : [];
|
|
1306
|
+
const tokenCount = Number.isFinite(Number(preview?.tokenCount))
|
|
1307
|
+
? Math.max(0, Math.round(Number(preview.tokenCount)))
|
|
1308
|
+
: null;
|
|
1309
|
+
if (lines.length) {
|
|
1310
|
+
return {
|
|
1311
|
+
lines: lines.slice(0, 3).map((line) => String(line || "").trim()).filter(Boolean),
|
|
1312
|
+
tokenCount,
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
const type = String(nodeType || "").trim().toLowerCase();
|
|
1316
|
+
const output = fallbackOutput && typeof fallbackOutput === "object" ? fallbackOutput : {};
|
|
1317
|
+
const fallbackLines = [];
|
|
1318
|
+
if (type.startsWith("condition.")) {
|
|
1319
|
+
const branch = String(output.matchedPort || output.port || "").trim();
|
|
1320
|
+
if (branch) fallbackLines.push(`Branch: ${branch}`);
|
|
1321
|
+
if (Object.prototype.hasOwnProperty.call(output, "value")) {
|
|
1322
|
+
fallbackLines.push(`Value: ${String(output.value)}`);
|
|
1323
|
+
}
|
|
1324
|
+
} else if (type.startsWith("git.") || type.startsWith("github.")) {
|
|
1325
|
+
const shaRaw = String(output.commitSha || output.sha || output.head || "").trim();
|
|
1326
|
+
const sha = /^[0-9a-f]{7,40}$/i.test(shaRaw) ? shaRaw.slice(0, 12) : "";
|
|
1327
|
+
if (sha) fallbackLines.push(`Commit: ${sha}`);
|
|
1328
|
+
const prUrl = String(output.prUrl || output.url || "").trim();
|
|
1329
|
+
if (prUrl) fallbackLines.push(`PR: ${prUrl}`);
|
|
1330
|
+
}
|
|
1331
|
+
if (!fallbackLines.length) {
|
|
1332
|
+
const text = String(output.summary || output.output || output.message || output.error || "").trim();
|
|
1333
|
+
if (text) fallbackLines.push(...text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 3));
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
lines: fallbackLines.slice(0, 3),
|
|
1337
|
+
tokenCount,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1210
1341
|
/* ═══════════════════════════════════════════════════════════════
|
|
1211
1342
|
* Canvas — SVG-based Workflow Editor
|
|
1212
1343
|
* ═══════════════════════════════════════════════════════════════ */
|
|
@@ -1231,6 +1362,14 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1231
1362
|
const [selectedNodeIds, setSelectedNodeIds] = useState(new Set());
|
|
1232
1363
|
const [historyState, setHistoryState] = useState(() => createHistoryState(workflow?.nodes || [], workflow?.edges || []));
|
|
1233
1364
|
const [marquee, setMarquee] = useState(null);
|
|
1365
|
+
const [liveHighlightEnabled, setLiveHighlightEnabled] = useState(true);
|
|
1366
|
+
const [liveRun, setLiveRun] = useState(null);
|
|
1367
|
+
const [liveNodeStatuses, setLiveNodeStatuses] = useState({});
|
|
1368
|
+
const [liveNodeOutputPreviews, setLiveNodeOutputPreviews] = useState({});
|
|
1369
|
+
const [liveNodeFlashStates, setLiveNodeFlashStates] = useState({});
|
|
1370
|
+
const [liveNodeRunningHints, setLiveNodeRunningHints] = useState({});
|
|
1371
|
+
const [liveEdgeActivity, setLiveEdgeActivity] = useState({});
|
|
1372
|
+
const [liveNowTick, setLiveNowTick] = useState(Date.now());
|
|
1234
1373
|
const marqueeStartRef = useRef(null);
|
|
1235
1374
|
const multiDragRef = useRef({});
|
|
1236
1375
|
const nodesRef = useRef(nodes);
|
|
@@ -1240,10 +1379,17 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1240
1379
|
const historyPendingSnapshotRef = useRef(null);
|
|
1241
1380
|
const saveTimer = useRef(null);
|
|
1242
1381
|
const selectedNodeIdsRef = useRef(selectedNodeIds);
|
|
1382
|
+
const liveEventQueueRef = useRef([]);
|
|
1383
|
+
const liveEventFlushTimerRef = useRef(null);
|
|
1243
1384
|
const workflowSnapshotKey = useMemo(
|
|
1244
1385
|
() => serializeGraphSnapshot(workflow?.nodes || [], workflow?.edges || []),
|
|
1245
1386
|
[workflow?.nodes, workflow?.edges],
|
|
1246
1387
|
);
|
|
1388
|
+
const hasLiveStatuses = Object.keys(liveNodeStatuses).length > 0;
|
|
1389
|
+
const liveRunDuration = liveRun?.status === "running" && liveRun?.startedAt
|
|
1390
|
+
? Math.max(0, liveNowTick - Number(liveRun.startedAt))
|
|
1391
|
+
: Number(liveRun?.duration) || 0;
|
|
1392
|
+
const liveActiveNodes = Object.values(liveNodeStatuses).filter((status) => status === "running").length;
|
|
1247
1393
|
useEffect(() => { selectedNodeIdsRef.current = selectedNodeIds; }, [selectedNodeIds]);
|
|
1248
1394
|
useEffect(() => {
|
|
1249
1395
|
nodesRef.current = nodes;
|
|
@@ -1268,11 +1414,293 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1268
1414
|
setEditingNode(null);
|
|
1269
1415
|
setContextMenu(null);
|
|
1270
1416
|
setShowNodePalette(false);
|
|
1417
|
+
setLiveRun(null);
|
|
1418
|
+
setLiveNodeStatuses({});
|
|
1419
|
+
setLiveNodeOutputPreviews({});
|
|
1420
|
+
setLiveNodeFlashStates({});
|
|
1421
|
+
setLiveNodeRunningHints({});
|
|
1422
|
+
setLiveEdgeActivity({});
|
|
1271
1423
|
}, [workflow?.id, workflowSnapshotKey]);
|
|
1272
1424
|
|
|
1425
|
+
useEffect(() => {
|
|
1426
|
+
if (!liveHighlightEnabled || !workflow?.id) {
|
|
1427
|
+
setLiveRun(null);
|
|
1428
|
+
setLiveNodeStatuses({});
|
|
1429
|
+
setLiveNodeOutputPreviews({});
|
|
1430
|
+
setLiveNodeFlashStates({});
|
|
1431
|
+
setLiveNodeRunningHints({});
|
|
1432
|
+
setLiveEdgeActivity({});
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
let cancelled = false;
|
|
1436
|
+
|
|
1437
|
+
const pollLiveRun = async () => {
|
|
1438
|
+
try {
|
|
1439
|
+
const data = await apiFetch(`/api/workflows/runs?workflowId=${encodeURIComponent(workflow.id)}&limit=10`);
|
|
1440
|
+
if (cancelled) return;
|
|
1441
|
+
const runs = Array.isArray(data?.runs) ? data.runs : [];
|
|
1442
|
+
const running = runs.find((run) => run?.status === "running");
|
|
1443
|
+
const targetRun = running || runs[0] || null;
|
|
1444
|
+
if (!targetRun?.runId) {
|
|
1445
|
+
setLiveRun(null);
|
|
1446
|
+
setLiveNodeStatuses({});
|
|
1447
|
+
setLiveNodeRunningHints({});
|
|
1448
|
+
setLiveNodeOutputPreviews({});
|
|
1449
|
+
setLiveNodeFlashStates({});
|
|
1450
|
+
setLiveEdgeActivity({});
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (targetRun.status !== "running") {
|
|
1454
|
+
setLiveRun(targetRun);
|
|
1455
|
+
setLiveNodeStatuses({});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const detailResponse = await apiFetch(`/api/workflows/runs/${targetRun.runId}`);
|
|
1459
|
+
if (cancelled) return;
|
|
1460
|
+
const detailedRun = detailResponse?.run || targetRun;
|
|
1461
|
+
setLiveRun(detailedRun);
|
|
1462
|
+
const runStatuses = buildNodeStatusesFromRunDetail(detailedRun);
|
|
1463
|
+
const normalizedStatuses = {};
|
|
1464
|
+
for (const [nodeId, status] of Object.entries(runStatuses || {})) {
|
|
1465
|
+
normalizedStatuses[nodeId] = normalizeLiveNodeStatus(status);
|
|
1466
|
+
}
|
|
1467
|
+
setLiveNodeStatuses(normalizedStatuses);
|
|
1468
|
+
const nodeOutputs = detailedRun?.detail?.nodeOutputs && typeof detailedRun.detail.nodeOutputs === "object"
|
|
1469
|
+
? detailedRun.detail.nodeOutputs
|
|
1470
|
+
: {};
|
|
1471
|
+
setLiveNodeOutputPreviews((prev) => {
|
|
1472
|
+
const next = { ...prev };
|
|
1473
|
+
for (const node of nodesRef.current || []) {
|
|
1474
|
+
const nodeId = String(node?.id || "").trim();
|
|
1475
|
+
if (!nodeId || !Object.prototype.hasOwnProperty.call(nodeOutputs, nodeId)) continue;
|
|
1476
|
+
const preview = resolveNodeOutputPreview(node?.type, null, nodeOutputs[nodeId]);
|
|
1477
|
+
const lines = Array.isArray(preview?.lines)
|
|
1478
|
+
? preview.lines.map((line) => String(line || "").trim()).filter(Boolean).slice(0, 3)
|
|
1479
|
+
: [];
|
|
1480
|
+
if (!lines.length && preview?.tokenCount == null) continue;
|
|
1481
|
+
next[nodeId] = {
|
|
1482
|
+
lines,
|
|
1483
|
+
tokenCount: Number.isFinite(Number(preview?.tokenCount))
|
|
1484
|
+
? Math.max(0, Math.round(Number(preview.tokenCount)))
|
|
1485
|
+
: null,
|
|
1486
|
+
updatedAt: Date.now(),
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
return next;
|
|
1490
|
+
});
|
|
1491
|
+
} catch {
|
|
1492
|
+
if (cancelled) return;
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
pollLiveRun();
|
|
1497
|
+
const pollTimer = setInterval(pollLiveRun, WORKFLOW_LIVE_POLL_MS);
|
|
1498
|
+
return () => {
|
|
1499
|
+
cancelled = true;
|
|
1500
|
+
clearInterval(pollTimer);
|
|
1501
|
+
};
|
|
1502
|
+
}, [liveHighlightEnabled, workflow?.id]);
|
|
1503
|
+
|
|
1504
|
+
useEffect(() => {
|
|
1505
|
+
if (!liveHighlightEnabled) return undefined;
|
|
1506
|
+
const timer = setInterval(() => setLiveNowTick(Date.now()), 1000);
|
|
1507
|
+
return () => clearInterval(timer);
|
|
1508
|
+
}, [liveHighlightEnabled, liveRun?.status]);
|
|
1509
|
+
|
|
1510
|
+
useEffect(() => {
|
|
1511
|
+
if (!liveHighlightEnabled) return;
|
|
1512
|
+
const now = Date.now();
|
|
1513
|
+
setLiveNodeFlashStates((prev) => {
|
|
1514
|
+
let changed = false;
|
|
1515
|
+
const next = { ...prev };
|
|
1516
|
+
for (const [nodeId, flash] of Object.entries(next)) {
|
|
1517
|
+
if (!flash || Number(flash.until) <= now) {
|
|
1518
|
+
delete next[nodeId];
|
|
1519
|
+
changed = true;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return changed ? next : prev;
|
|
1523
|
+
});
|
|
1524
|
+
setLiveNodeRunningHints((prev) => {
|
|
1525
|
+
let changed = false;
|
|
1526
|
+
const next = { ...prev };
|
|
1527
|
+
for (const [nodeId, until] of Object.entries(next)) {
|
|
1528
|
+
if (Number(until || 0) <= now) {
|
|
1529
|
+
delete next[nodeId];
|
|
1530
|
+
changed = true;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return changed ? next : prev;
|
|
1534
|
+
});
|
|
1535
|
+
setLiveEdgeActivity((prev) => {
|
|
1536
|
+
let changed = false;
|
|
1537
|
+
const next = { ...prev };
|
|
1538
|
+
for (const [edgeId, info] of Object.entries(next)) {
|
|
1539
|
+
if (!info || now - Number(info.ts || 0) > EDGE_FLOW_ANIMATION_MS) {
|
|
1540
|
+
delete next[edgeId];
|
|
1541
|
+
changed = true;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return changed ? next : prev;
|
|
1545
|
+
});
|
|
1546
|
+
}, [liveNowTick, liveHighlightEnabled]);
|
|
1547
|
+
|
|
1548
|
+
useEffect(() => {
|
|
1549
|
+
if (!liveHighlightEnabled || !workflow?.id) return undefined;
|
|
1550
|
+
const flushQueuedEvents = () => {
|
|
1551
|
+
if (liveEventFlushTimerRef.current) {
|
|
1552
|
+
clearTimeout(liveEventFlushTimerRef.current);
|
|
1553
|
+
liveEventFlushTimerRef.current = null;
|
|
1554
|
+
}
|
|
1555
|
+
const queued = liveEventQueueRef.current.splice(0, liveEventQueueRef.current.length);
|
|
1556
|
+
if (!queued.length) return;
|
|
1557
|
+
setLiveNowTick(Date.now());
|
|
1558
|
+
setLiveRun((prev) => {
|
|
1559
|
+
let next = prev;
|
|
1560
|
+
for (const event of queued) {
|
|
1561
|
+
if (event.kind !== "run") continue;
|
|
1562
|
+
if (!next || next.runId !== event.runId) {
|
|
1563
|
+
next = {
|
|
1564
|
+
...(next || {}),
|
|
1565
|
+
runId: event.runId,
|
|
1566
|
+
workflowId: event.workflowId || workflow.id,
|
|
1567
|
+
workflowName: event.workflowName || workflow.name,
|
|
1568
|
+
startedAt: event.timestamp || Date.now(),
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
next = {
|
|
1572
|
+
...next,
|
|
1573
|
+
runId: event.runId,
|
|
1574
|
+
workflowId: event.workflowId || next.workflowId || workflow.id,
|
|
1575
|
+
workflowName: event.workflowName || next.workflowName || workflow.name,
|
|
1576
|
+
status: event.status || next.status || "running",
|
|
1577
|
+
duration: Number.isFinite(Number(event.duration)) ? Number(event.duration) : next.duration,
|
|
1578
|
+
endedAt: event.status && event.status !== "running"
|
|
1579
|
+
? (event.timestamp || Date.now())
|
|
1580
|
+
: next.endedAt,
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
return next;
|
|
1584
|
+
});
|
|
1585
|
+
setLiveNodeStatuses((prev) => {
|
|
1586
|
+
const next = { ...prev };
|
|
1587
|
+
for (const event of queued) {
|
|
1588
|
+
if (event.kind !== "node" || !event.nodeId) continue;
|
|
1589
|
+
next[event.nodeId] = normalizeLiveNodeStatus(event.status);
|
|
1590
|
+
}
|
|
1591
|
+
return next;
|
|
1592
|
+
});
|
|
1593
|
+
setLiveNodeOutputPreviews((prev) => {
|
|
1594
|
+
const next = { ...prev };
|
|
1595
|
+
for (const event of queued) {
|
|
1596
|
+
if (event.kind !== "node" || !event.nodeId) continue;
|
|
1597
|
+
if (event.outputPreview || event.error) {
|
|
1598
|
+
const lines = Array.isArray(event.outputPreview?.lines)
|
|
1599
|
+
? event.outputPreview.lines
|
|
1600
|
+
: (event.error ? [String(event.error)] : []);
|
|
1601
|
+
next[event.nodeId] = {
|
|
1602
|
+
lines: lines.slice(0, 3),
|
|
1603
|
+
tokenCount: Number.isFinite(Number(event.outputPreview?.tokenCount))
|
|
1604
|
+
? Math.max(0, Math.round(Number(event.outputPreview.tokenCount)))
|
|
1605
|
+
: null,
|
|
1606
|
+
updatedAt: event.timestamp || Date.now(),
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return next;
|
|
1611
|
+
});
|
|
1612
|
+
setLiveNodeFlashStates((prev) => {
|
|
1613
|
+
const next = { ...prev };
|
|
1614
|
+
const now = Date.now();
|
|
1615
|
+
for (const event of queued) {
|
|
1616
|
+
if (event.kind !== "node" || !event.nodeId) continue;
|
|
1617
|
+
const normalized = normalizeLiveNodeStatus(event.status);
|
|
1618
|
+
if (normalized === "success" || normalized === "fail" || normalized === "skipped") {
|
|
1619
|
+
next[event.nodeId] = {
|
|
1620
|
+
state: normalized,
|
|
1621
|
+
until: now + NODE_COMPLETION_FLASH_MS,
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
for (const [nodeId, flash] of Object.entries(next)) {
|
|
1626
|
+
if (!flash || Number(flash.until) <= now) delete next[nodeId];
|
|
1627
|
+
}
|
|
1628
|
+
return next;
|
|
1629
|
+
});
|
|
1630
|
+
setLiveNodeRunningHints((prev) => {
|
|
1631
|
+
const next = { ...prev };
|
|
1632
|
+
const now = Date.now();
|
|
1633
|
+
let changed = false;
|
|
1634
|
+
for (const event of queued) {
|
|
1635
|
+
if (event.kind !== "node" || !event.nodeId) continue;
|
|
1636
|
+
const normalized = normalizeLiveNodeStatus(event.status);
|
|
1637
|
+
if (normalized === "running") {
|
|
1638
|
+
next[event.nodeId] = now + NODE_RUNNING_HINT_MS;
|
|
1639
|
+
changed = true;
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
if (normalized === "success" || normalized === "fail" || normalized === "skipped") {
|
|
1643
|
+
if (next[event.nodeId]) {
|
|
1644
|
+
delete next[event.nodeId];
|
|
1645
|
+
changed = true;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return changed ? next : prev;
|
|
1650
|
+
});
|
|
1651
|
+
setLiveEdgeActivity((prev) => {
|
|
1652
|
+
const next = { ...prev };
|
|
1653
|
+
const now = Date.now();
|
|
1654
|
+
for (const event of queued) {
|
|
1655
|
+
if (event.kind !== "edge" || !event.edgeId) continue;
|
|
1656
|
+
next[event.edgeId] = {
|
|
1657
|
+
ts: Number(event.timestamp) || now,
|
|
1658
|
+
source: event.source || null,
|
|
1659
|
+
target: event.target || null,
|
|
1660
|
+
reason: event.reason || "flow",
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
for (const [edgeId, info] of Object.entries(next)) {
|
|
1664
|
+
if (!info || now - Number(info.ts || 0) > EDGE_FLOW_ANIMATION_MS) {
|
|
1665
|
+
delete next[edgeId];
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return next;
|
|
1669
|
+
});
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
const scheduleEventFlush = () => {
|
|
1673
|
+
if (liveEventFlushTimerRef.current) return;
|
|
1674
|
+
liveEventFlushTimerRef.current = setTimeout(flushQueuedEvents, WORKFLOW_LIVE_WS_BATCH_MS);
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
const unsub = onWsMessage((msg) => {
|
|
1678
|
+
if (msg?.type !== "workflow-run-events") return;
|
|
1679
|
+
const payload = msg?.payload || {};
|
|
1680
|
+
const payloadWorkflowId = String(payload.workflowId || "").trim();
|
|
1681
|
+
if (payloadWorkflowId !== String(workflow.id || "").trim()) return;
|
|
1682
|
+
const events = Array.isArray(payload.events) ? payload.events : [];
|
|
1683
|
+
if (!events.length) return;
|
|
1684
|
+
liveEventQueueRef.current.push(...events);
|
|
1685
|
+
scheduleEventFlush();
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
return () => {
|
|
1689
|
+
if (liveEventFlushTimerRef.current) {
|
|
1690
|
+
clearTimeout(liveEventFlushTimerRef.current);
|
|
1691
|
+
liveEventFlushTimerRef.current = null;
|
|
1692
|
+
}
|
|
1693
|
+
liveEventQueueRef.current = [];
|
|
1694
|
+
try {
|
|
1695
|
+
unsub?.();
|
|
1696
|
+
} catch {}
|
|
1697
|
+
};
|
|
1698
|
+
}, [liveHighlightEnabled, workflow?.id, workflow?.name]);
|
|
1699
|
+
|
|
1273
1700
|
// Canvas dimensions
|
|
1274
1701
|
const NODE_W = 220;
|
|
1275
|
-
const NODE_H =
|
|
1702
|
+
const NODE_H = 138;
|
|
1703
|
+
const NODE_HEADER_H = 62;
|
|
1276
1704
|
const PORT_R = 8;
|
|
1277
1705
|
const HISTORY_LIMIT = 50;
|
|
1278
1706
|
const HISTORY_COMMIT_DEBOUNCE_MS = 220;
|
|
@@ -1932,7 +2360,7 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1932
2360
|
<span class="btn-icon">${resolveIcon("play")}</span>
|
|
1933
2361
|
Run
|
|
1934
2362
|
<//>
|
|
1935
|
-
|
|
2363
|
+
${workflow?.core !== true && html`<${Button}
|
|
1936
2364
|
variant="outlined"
|
|
1937
2365
|
size="small"
|
|
1938
2366
|
onClick=${() => {
|
|
@@ -1942,7 +2370,8 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1942
2370
|
>
|
|
1943
2371
|
<span class="btn-icon">${resolveIcon(workflow?.enabled === false ? "play" : "pause")}</span>
|
|
1944
2372
|
${workflow?.enabled === false ? "Resume" : "Pause"}
|
|
1945
|
-
|
|
2373
|
+
<//>`}
|
|
2374
|
+
${workflow?.core === true && html`<span class="wf-badge" style="background: #8b5cf620; color: #a78bfa; font-size: 11px; font-weight: 600;">Core</span>`}
|
|
1946
2375
|
<${Button} variant="text" size="small" disabled=${historyState.past.length === 0} onClick=${undoCanvas}>Undo<//>
|
|
1947
2376
|
<${Button} variant="text" size="small" disabled=${historyState.future.length === 0} onClick=${redoCanvas}>Redo<//>
|
|
1948
2377
|
<${Button} variant="text" size="small" onClick=${() => setShowShortcutOverlay(true)}>Shortcuts ?<//>
|
|
@@ -1958,6 +2387,24 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1958
2387
|
<span class="wf-badge" style="font-size: 11px; opacity: 0.75;">
|
|
1959
2388
|
${workflow?.enabled === false ? "Paused" : "Active"} · Pan: touch drag, Ctrl/Space + drag
|
|
1960
2389
|
</span>
|
|
2390
|
+
<div style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--color-text-secondary, #8b95a5);">
|
|
2391
|
+
<${Switch}
|
|
2392
|
+
size="small"
|
|
2393
|
+
checked=${liveHighlightEnabled}
|
|
2394
|
+
onChange=${(e) => setLiveHighlightEnabled(Boolean(e.target.checked))}
|
|
2395
|
+
/>
|
|
2396
|
+
<span>Live highlights</span>
|
|
2397
|
+
</div>
|
|
2398
|
+
${liveHighlightEnabled && liveRun?.runId && html`
|
|
2399
|
+
<span class="wf-badge" style="font-size: 11px; background: ${getRunStatusBadgeStyles(liveRun.status).bg}; color: ${getRunStatusBadgeStyles(liveRun.status).color};">
|
|
2400
|
+
${liveRun.status === "running" ? "Live Run" : "Last Run"} · ${formatDuration(liveRunDuration)}
|
|
2401
|
+
</span>
|
|
2402
|
+
`}
|
|
2403
|
+
${liveHighlightEnabled && hasLiveStatuses && html`
|
|
2404
|
+
<span class="wf-badge" style="font-size: 11px; background: #3b82f630; color: #60a5fa;">
|
|
2405
|
+
${liveActiveNodes} active node${liveActiveNodes === 1 ? "" : "s"}
|
|
2406
|
+
</span>
|
|
2407
|
+
`}
|
|
1961
2408
|
<${Button} variant="text" size="small" onClick=${() => setZoom(1)}>Reset Zoom<//>
|
|
1962
2409
|
<${Button} variant="text" size="small" onClick=${() => setPan({ x: 0, y: 0 })}>Reset Pan<//>
|
|
1963
2410
|
<${Button} variant="text" size="small" onClick=${returnToWorkflowList}>← Back to Workflows<//>
|
|
@@ -2020,25 +2467,48 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2020
2467
|
const to = getInputPort(edge.target);
|
|
2021
2468
|
const isSelected = selectedEdgeId.value === edge.id;
|
|
2022
2469
|
const hasCondition = !!edge.condition;
|
|
2470
|
+
const edgeKey = edge.id || `${edge.source}->${edge.target}`;
|
|
2471
|
+
const edgeActivity = liveHighlightEnabled ? liveEdgeActivity[edgeKey] : null;
|
|
2472
|
+
const isActiveFlow = Boolean(edgeActivity);
|
|
2473
|
+
const edgePath = curvePath(from.x, from.y, to.x, to.y);
|
|
2023
2474
|
return html`
|
|
2024
2475
|
<g key=${edge.id} class="wf-edge" onClick=${(e) => { e.stopPropagation(); selectedEdgeId.value = edge.id; }}>
|
|
2025
2476
|
<path
|
|
2026
|
-
d=${
|
|
2477
|
+
d=${edgePath}
|
|
2027
2478
|
fill="none"
|
|
2028
|
-
stroke=${isSelected ? "#3b82f6" : hasCondition ? "#f59e0b" : "#6b7280"}
|
|
2029
|
-
stroke-width=${isSelected ? 3 : 2}
|
|
2479
|
+
stroke=${isSelected ? "#3b82f6" : isActiveFlow ? "#60a5fa" : hasCondition ? "#f59e0b" : "#6b7280"}
|
|
2480
|
+
stroke-width=${isSelected ? 3 : isActiveFlow ? 2.8 : 2}
|
|
2030
2481
|
stroke-dasharray=${hasCondition ? "6,4" : "none"}
|
|
2031
2482
|
marker-end="url(#arrowhead)"
|
|
2032
|
-
style
|
|
2483
|
+
style=${`cursor: pointer; transition: stroke 0.15s, stroke-width 0.15s; ${isActiveFlow ? "filter: drop-shadow(0 0 6px rgba(96,165,250,0.45));" : ""}`}
|
|
2033
2484
|
/>
|
|
2485
|
+
${isActiveFlow && html`
|
|
2486
|
+
<path
|
|
2487
|
+
d=${edgePath}
|
|
2488
|
+
fill="none"
|
|
2489
|
+
stroke="#93c5fd"
|
|
2490
|
+
stroke-width="1.6"
|
|
2491
|
+
stroke-dasharray="12,8"
|
|
2492
|
+
marker-end="url(#arrowhead)"
|
|
2493
|
+
opacity="0.9"
|
|
2494
|
+
style="pointer-events: none;"
|
|
2495
|
+
>
|
|
2496
|
+
<animate attributeName="stroke-dashoffset" values="0;-20" dur="0.45s" repeatCount="indefinite" />
|
|
2497
|
+
</path>
|
|
2498
|
+
`}
|
|
2034
2499
|
<!-- Invisible wider hit area -->
|
|
2035
2500
|
<path
|
|
2036
|
-
d=${
|
|
2501
|
+
d=${edgePath}
|
|
2037
2502
|
fill="none"
|
|
2038
2503
|
stroke="transparent"
|
|
2039
2504
|
stroke-width="12"
|
|
2040
2505
|
style="cursor: pointer;"
|
|
2041
2506
|
/>
|
|
2507
|
+
${isActiveFlow && html`
|
|
2508
|
+
<circle r="3.4" fill="#93c5fd" opacity="0.95">
|
|
2509
|
+
<animateMotion dur="0.95s" repeatCount="1" rotate="auto" path=${edgePath} />
|
|
2510
|
+
</circle>
|
|
2511
|
+
`}
|
|
2042
2512
|
${hasCondition && html`
|
|
2043
2513
|
<text
|
|
2044
2514
|
x=${(from.x + to.x) / 2}
|
|
@@ -2081,29 +2551,58 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2081
2551
|
${nodes.map(node => {
|
|
2082
2552
|
const meta = getNodeMeta(node.type);
|
|
2083
2553
|
const isSelected = selectedNodeIds.has(node.id);
|
|
2554
|
+
const nodeRunStatus = liveHighlightEnabled ? normalizeLiveNodeStatus(liveNodeStatuses[node.id]) : null;
|
|
2555
|
+
const nodeFlash = liveNodeFlashStates[node.id] || null;
|
|
2556
|
+
const flashState = nodeFlash?.state || "";
|
|
2557
|
+
const executionVisuals = getCanvasNodeExecutionVisuals(nodeRunStatus, isSelected, meta.color, flashState);
|
|
2558
|
+
const nodeStatusStyles = getRunStatusBadgeStyles(nodeRunStatus);
|
|
2559
|
+
const preview = resolveNodeOutputPreview(node.type, liveNodeOutputPreviews[node.id], null);
|
|
2560
|
+
const previewLines = preview.lines.slice(0, 3);
|
|
2561
|
+
const hasPreview = previewLines.length > 0 || preview.tokenCount != null;
|
|
2562
|
+
const runningHintUntil = Number(liveNodeRunningHints[node.id] || 0);
|
|
2563
|
+
const hasRunningHint = runningHintUntil > liveNowTick;
|
|
2564
|
+
const spinnerVisible = nodeRunStatus === "running" || hasRunningHint;
|
|
2565
|
+
const previewPanelY = NODE_HEADER_H + 8;
|
|
2566
|
+
const previewPanelH = Math.max(30, NODE_H - previewPanelY - 8);
|
|
2084
2567
|
const x = node.position?.x || 0;
|
|
2085
2568
|
const y = node.position?.y || 0;
|
|
2086
2569
|
return html`
|
|
2087
2570
|
<g
|
|
2088
2571
|
key=${node.id}
|
|
2089
|
-
class
|
|
2572
|
+
class=${`wf-node${spinnerVisible ? " wf-node-running" : ""}${flashState ? ` wf-node-flash-${flashState}` : ""}`}
|
|
2090
2573
|
transform="translate(${x} ${y})"
|
|
2091
2574
|
onMouseDown=${(e) => onNodeMouseDown(node.id, e)}
|
|
2092
2575
|
onPointerDown=${(e) => onNodePointerDown(node.id, e)}
|
|
2093
2576
|
onDblClick=${() => onNodeDoubleClick(node.id)}
|
|
2094
2577
|
onContextMenu=${(e) => onNodeContextMenu(node.id, e)}
|
|
2095
2578
|
style="cursor: grab;"
|
|
2096
|
-
filter=${
|
|
2579
|
+
filter=${executionVisuals.filter}
|
|
2097
2580
|
>
|
|
2098
2581
|
<!-- Node body -->
|
|
2099
2582
|
<rect
|
|
2100
2583
|
width=${NODE_W}
|
|
2101
2584
|
height=${NODE_H}
|
|
2102
2585
|
rx="8"
|
|
2103
|
-
fill=${
|
|
2104
|
-
stroke=${
|
|
2105
|
-
stroke-width=${
|
|
2586
|
+
fill=${executionVisuals.fill}
|
|
2587
|
+
stroke=${executionVisuals.stroke}
|
|
2588
|
+
stroke-width=${executionVisuals.strokeWidth}
|
|
2106
2589
|
/>
|
|
2590
|
+
${spinnerVisible && html`
|
|
2591
|
+
<rect
|
|
2592
|
+
x="1.5"
|
|
2593
|
+
y="1.5"
|
|
2594
|
+
width=${NODE_W - 3}
|
|
2595
|
+
height=${NODE_H - 3}
|
|
2596
|
+
rx="7"
|
|
2597
|
+
fill="none"
|
|
2598
|
+
stroke="#93c5fd"
|
|
2599
|
+
stroke-opacity="0.85"
|
|
2600
|
+
stroke-width="1.6"
|
|
2601
|
+
stroke-dasharray="10 6"
|
|
2602
|
+
>
|
|
2603
|
+
<animate attributeName="stroke-dashoffset" values="0;-32" dur="1s" repeatCount="indefinite" />
|
|
2604
|
+
</rect>
|
|
2605
|
+
`}
|
|
2107
2606
|
|
|
2108
2607
|
<!-- Category color strip -->
|
|
2109
2608
|
<rect
|
|
@@ -2116,7 +2615,7 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2116
2615
|
<!-- Label -->
|
|
2117
2616
|
<text
|
|
2118
2617
|
x=${NODE_W / 2}
|
|
2119
|
-
y
|
|
2618
|
+
y="22"
|
|
2120
2619
|
text-anchor="middle"
|
|
2121
2620
|
fill="white"
|
|
2122
2621
|
font-size="13"
|
|
@@ -2126,12 +2625,73 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2126
2625
|
<!-- Type subtitle -->
|
|
2127
2626
|
<text
|
|
2128
2627
|
x=${NODE_W / 2}
|
|
2129
|
-
y
|
|
2628
|
+
y="40"
|
|
2130
2629
|
text-anchor="middle"
|
|
2131
2630
|
fill="#94a3b8"
|
|
2132
2631
|
font-size="10"
|
|
2133
2632
|
>${node.type}</text>
|
|
2134
2633
|
|
|
2634
|
+
${nodeRunStatus && html`
|
|
2635
|
+
<g transform="translate(${NODE_W - 74} 8)">
|
|
2636
|
+
<rect
|
|
2637
|
+
width="66"
|
|
2638
|
+
height="18"
|
|
2639
|
+
rx="9"
|
|
2640
|
+
fill=${nodeStatusStyles.bg}
|
|
2641
|
+
stroke=${nodeStatusStyles.color}
|
|
2642
|
+
stroke-opacity="0.4"
|
|
2643
|
+
/>
|
|
2644
|
+
<text
|
|
2645
|
+
x="33"
|
|
2646
|
+
y="12"
|
|
2647
|
+
text-anchor="middle"
|
|
2648
|
+
fill=${nodeStatusStyles.color}
|
|
2649
|
+
font-size="10"
|
|
2650
|
+
font-weight="700"
|
|
2651
|
+
>${toNodeStatusLabel(nodeRunStatus)}</text>
|
|
2652
|
+
</g>
|
|
2653
|
+
`}
|
|
2654
|
+
|
|
2655
|
+
${spinnerVisible && html`
|
|
2656
|
+
<g transform="translate(${NODE_W - 22} ${NODE_HEADER_H - 18})">
|
|
2657
|
+
<circle r="9" cx="0" cy="0" fill="rgba(15, 23, 42, 0.72)" stroke="#93c5fd44" stroke-width="1.2" />
|
|
2658
|
+
<circle r="6.8" cx="0" cy="0" fill="none" stroke="#93c5fd33" stroke-width="1.2" />
|
|
2659
|
+
<path d="M 0 -6.8 A 6.8 6.8 0 0 1 5.4 -4.2" fill="none" stroke="#93c5fd" stroke-width="1.8" stroke-linecap="round">
|
|
2660
|
+
<animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="0.8s" repeatCount="indefinite" />
|
|
2661
|
+
</path>
|
|
2662
|
+
</g>
|
|
2663
|
+
`}
|
|
2664
|
+
|
|
2665
|
+
<line x1="8" y1=${NODE_HEADER_H} x2=${NODE_W - 8} y2=${NODE_HEADER_H} stroke="#2e3748" stroke-width="1" opacity="0.85" />
|
|
2666
|
+
<rect
|
|
2667
|
+
x="8"
|
|
2668
|
+
y=${previewPanelY}
|
|
2669
|
+
width=${NODE_W - 16}
|
|
2670
|
+
height=${previewPanelH}
|
|
2671
|
+
rx="6"
|
|
2672
|
+
fill="rgba(15, 23, 42, 0.45)"
|
|
2673
|
+
stroke=${hasPreview ? "rgba(148, 163, 184, 0.28)" : "rgba(71, 85, 105, 0.2)"}
|
|
2674
|
+
stroke-width="1"
|
|
2675
|
+
/>
|
|
2676
|
+
|
|
2677
|
+
${hasPreview && html`
|
|
2678
|
+
<text x="14" y=${previewPanelY + 14} fill="#cbd5e1" font-size="10" font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace">
|
|
2679
|
+
${previewLines.map((line, index) => html`
|
|
2680
|
+
<tspan x="14" dy=${index === 0 ? 0 : 13}>${line.slice(0, 44)}</tspan>
|
|
2681
|
+
`)}
|
|
2682
|
+
</text>
|
|
2683
|
+
`}
|
|
2684
|
+
${preview.tokenCount != null && html`
|
|
2685
|
+
<text
|
|
2686
|
+
x=${NODE_W - 14}
|
|
2687
|
+
y=${NODE_H - 10}
|
|
2688
|
+
text-anchor="end"
|
|
2689
|
+
fill="#93c5fd"
|
|
2690
|
+
font-size="10"
|
|
2691
|
+
font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
|
|
2692
|
+
>${preview.tokenCount} tok</text>
|
|
2693
|
+
`}
|
|
2694
|
+
|
|
2135
2695
|
<!-- Input port (left) -->
|
|
2136
2696
|
<circle
|
|
2137
2697
|
cx="0"
|
|
@@ -2381,14 +2941,45 @@ function KeyboardShortcutOverlay({ open, onClose, canUndo, canRedo }) {
|
|
|
2381
2941
|
|
|
2382
2942
|
const COMMAND_PRESETS = {
|
|
2383
2943
|
testing: [
|
|
2384
|
-
{ label: "Run Tests", cmd: "npm test", icon: "beaker" },
|
|
2944
|
+
{ label: "Run Tests (npm)", cmd: "npm test", icon: "beaker" },
|
|
2945
|
+
{ label: "Run Tests (yarn)", cmd: "yarn test", icon: "beaker" },
|
|
2946
|
+
{ label: "Run Tests (pnpm)", cmd: "pnpm test", icon: "beaker" },
|
|
2947
|
+
{ label: "Run Tests (pytest)", cmd: "pytest", icon: "beaker" },
|
|
2948
|
+
{ label: "Run Tests (Go)", cmd: "go test ./...", icon: "beaker" },
|
|
2949
|
+
{ label: "Run Tests (Rust)", cmd: "cargo test", icon: "beaker" },
|
|
2950
|
+
{ label: "Run Tests (Java/Maven)", cmd: "mvn test", icon: "beaker" },
|
|
2951
|
+
{ label: "Run Tests (Java/Gradle)", cmd: "./gradlew test", icon: "beaker" },
|
|
2952
|
+
{ label: "Run Tests (.NET)", cmd: "dotnet test", icon: "beaker" },
|
|
2953
|
+
{ label: "Run Tests (Ruby)", cmd: "bundle exec rspec", icon: "beaker" },
|
|
2385
2954
|
{ label: "Run Single File", cmd: 'npx vitest run tests/{{testFile}}', icon: "target" },
|
|
2386
|
-
{ label: "Syntax Check", cmd: "npm run syntax:check", icon: "check" },
|
|
2955
|
+
{ label: "Syntax Check (Node)", cmd: "npm run syntax:check", icon: "check" },
|
|
2956
|
+
{ label: "Syntax Check (Python)", cmd: "python -m py_compile", icon: "check" },
|
|
2957
|
+
{ label: "Syntax Check (Go)", cmd: "go vet ./...", icon: "check" },
|
|
2958
|
+
{ label: "Syntax Check (Rust)", cmd: "cargo check", icon: "check" },
|
|
2387
2959
|
],
|
|
2388
2960
|
build: [
|
|
2389
|
-
{ label: "Build
|
|
2961
|
+
{ label: "Build (npm)", cmd: "npm run build", icon: "hammer" },
|
|
2962
|
+
{ label: "Build (yarn)", cmd: "yarn build", icon: "hammer" },
|
|
2963
|
+
{ label: "Build (pnpm)", cmd: "pnpm build", icon: "hammer" },
|
|
2964
|
+
{ label: "Build (Go)", cmd: "go build ./...", icon: "hammer" },
|
|
2965
|
+
{ label: "Build (Rust)", cmd: "cargo build", icon: "hammer" },
|
|
2966
|
+
{ label: "Build (Maven)", cmd: "mvn package -DskipTests", icon: "hammer" },
|
|
2967
|
+
{ label: "Build (Gradle)", cmd: "./gradlew build", icon: "hammer" },
|
|
2968
|
+
{ label: "Build (.NET)", cmd: "dotnet build", icon: "hammer" },
|
|
2969
|
+
{ label: "Build (Python)", cmd: "python -m build", icon: "hammer" },
|
|
2970
|
+
{ label: "Build (Make)", cmd: "make", icon: "hammer" },
|
|
2390
2971
|
{ label: "Build Watch", cmd: "npm run build -- --watch", icon: "eye" },
|
|
2391
|
-
{ label: "Type Check", cmd: "npx tsc --noEmit", icon: "ruler" },
|
|
2972
|
+
{ label: "Type Check (TS)", cmd: "npx tsc --noEmit", icon: "ruler" },
|
|
2973
|
+
],
|
|
2974
|
+
lint: [
|
|
2975
|
+
{ label: "Lint (npm)", cmd: "npm run lint", icon: "search" },
|
|
2976
|
+
{ label: "Lint (ESLint)", cmd: "npx eslint .", icon: "search" },
|
|
2977
|
+
{ label: "Lint (Python/Ruff)", cmd: "ruff check .", icon: "search" },
|
|
2978
|
+
{ label: "Lint (Python/Flake8)", cmd: "flake8", icon: "search" },
|
|
2979
|
+
{ label: "Lint (Go)", cmd: "golangci-lint run", icon: "search" },
|
|
2980
|
+
{ label: "Lint (Rust)", cmd: "cargo clippy -- -D warnings", icon: "search" },
|
|
2981
|
+
{ label: "Lint (Ruby)", cmd: "bundle exec rubocop", icon: "search" },
|
|
2982
|
+
{ label: "Lint (.NET)", cmd: "dotnet format --verify-no-changes", icon: "search" },
|
|
2392
2983
|
],
|
|
2393
2984
|
git: [
|
|
2394
2985
|
{ label: "Diff Stats", cmd: "git diff --stat main...HEAD", icon: "chart" },
|
|
@@ -2858,16 +3449,33 @@ function NodeConfigEditor({ node, nodeTypes: types, onUpdate, onUpdateLabel, onC
|
|
|
2858
3449
|
${[
|
|
2859
3450
|
...(nodeAction === "build" ? [
|
|
2860
3451
|
{ label: "npm run build", cmd: "npm run build" },
|
|
3452
|
+
{ label: "yarn build", cmd: "yarn build" },
|
|
3453
|
+
{ label: "go build", cmd: "go build ./..." },
|
|
3454
|
+
{ label: "cargo build", cmd: "cargo build" },
|
|
3455
|
+
{ label: "mvn package", cmd: "mvn package -DskipTests" },
|
|
3456
|
+
{ label: "gradlew build", cmd: "./gradlew build" },
|
|
3457
|
+
{ label: "dotnet build", cmd: "dotnet build" },
|
|
3458
|
+
{ label: "make", cmd: "make" },
|
|
2861
3459
|
{ label: "Zero Warnings", cmd: "npm run build", extra: { zeroWarnings: true } },
|
|
2862
3460
|
] : []),
|
|
2863
3461
|
...(nodeAction === "tests" ? [
|
|
2864
3462
|
{ label: "npm test", cmd: "npm test" },
|
|
2865
3463
|
{ label: "Vitest", cmd: "npx vitest run" },
|
|
2866
3464
|
{ label: "Jest", cmd: "npx jest" },
|
|
3465
|
+
{ label: "pytest", cmd: "pytest" },
|
|
3466
|
+
{ label: "go test", cmd: "go test ./..." },
|
|
3467
|
+
{ label: "cargo test", cmd: "cargo test" },
|
|
3468
|
+
{ label: "mvn test", cmd: "mvn test" },
|
|
3469
|
+
{ label: "dotnet test", cmd: "dotnet test" },
|
|
3470
|
+
{ label: "rspec", cmd: "bundle exec rspec" },
|
|
2867
3471
|
] : []),
|
|
2868
3472
|
...(nodeAction === "lint" ? [
|
|
2869
3473
|
{ label: "npm run lint", cmd: "npm run lint" },
|
|
2870
3474
|
{ label: "ESLint", cmd: "npx eslint ." },
|
|
3475
|
+
{ label: "Ruff", cmd: "ruff check ." },
|
|
3476
|
+
{ label: "golangci-lint", cmd: "golangci-lint run" },
|
|
3477
|
+
{ label: "Clippy", cmd: "cargo clippy -- -D warnings" },
|
|
3478
|
+
{ label: "Rubocop", cmd: "bundle exec rubocop" },
|
|
2871
3479
|
] : []),
|
|
2872
3480
|
].map(p => html`
|
|
2873
3481
|
<${Button}
|
|
@@ -3017,6 +3625,52 @@ function NodeConfigEditor({ node, nodeTypes: types, onUpdate, onUpdateLabel, onC
|
|
|
3017
3625
|
* Workflow List View
|
|
3018
3626
|
* ═══════════════════════════════════════════════════════════════ */
|
|
3019
3627
|
|
|
3628
|
+
function humanizeWorkflowCategory(category) {
|
|
3629
|
+
const normalized = String(category || "custom").trim();
|
|
3630
|
+
if (!normalized) return "Custom";
|
|
3631
|
+
return normalized
|
|
3632
|
+
.replace(/[-_]+/g, " ")
|
|
3633
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
function normalizeWorkflowCategoryMeta(source, fallbackCategory = "custom") {
|
|
3637
|
+
const key = String(source?.category || fallbackCategory || "custom").trim() || "custom";
|
|
3638
|
+
const order = Number(source?.categoryOrder);
|
|
3639
|
+
return {
|
|
3640
|
+
key,
|
|
3641
|
+
label: String(source?.categoryLabel || humanizeWorkflowCategory(key)),
|
|
3642
|
+
icon: String(source?.categoryIcon || "settings"),
|
|
3643
|
+
order: Number.isFinite(order) ? order : 99,
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
function groupItemsByWorkflowCategory(items, getSource) {
|
|
3648
|
+
const groups = new Map();
|
|
3649
|
+
for (const item of items || []) {
|
|
3650
|
+
const meta = normalizeWorkflowCategoryMeta(getSource(item), item?.category);
|
|
3651
|
+
if (!groups.has(meta.key)) groups.set(meta.key, { ...meta, items: [] });
|
|
3652
|
+
groups.get(meta.key).items.push(item);
|
|
3653
|
+
}
|
|
3654
|
+
return Array.from(groups.values()).sort((a, b) => a.order - b.order || a.label.localeCompare(b.label));
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
function resolveWorkflowTemplateSource(workflow, templateLookupById, templateLookupByName) {
|
|
3658
|
+
const templateState = workflow?.metadata?.templateState || null;
|
|
3659
|
+
const candidates = [
|
|
3660
|
+
templateState?.templateId,
|
|
3661
|
+
workflow?.metadata?.installedFrom,
|
|
3662
|
+
templateState?.templateName,
|
|
3663
|
+
workflow?.name,
|
|
3664
|
+
];
|
|
3665
|
+
for (const candidate of candidates) {
|
|
3666
|
+
const key = String(candidate || "").trim();
|
|
3667
|
+
if (!key) continue;
|
|
3668
|
+
if (templateLookupById.has(key)) return templateLookupById.get(key);
|
|
3669
|
+
if (templateLookupByName.has(key)) return templateLookupByName.get(key);
|
|
3670
|
+
}
|
|
3671
|
+
return null;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3020
3674
|
function WorkflowListView() {
|
|
3021
3675
|
const wfs = workflows.value || [];
|
|
3022
3676
|
const tmpls = templates.value || [];
|
|
@@ -3029,6 +3683,28 @@ function WorkflowListView() {
|
|
|
3029
3683
|
if (installedTemplateIds.has(t.id) || installedTemplateIds.has(t.name)) return false;
|
|
3030
3684
|
return true;
|
|
3031
3685
|
});
|
|
3686
|
+
const templateLookup = useMemo(() => {
|
|
3687
|
+
const byId = new Map();
|
|
3688
|
+
const byName = new Map();
|
|
3689
|
+
tmpls.forEach((template) => {
|
|
3690
|
+
const id = String(template?.id || "").trim();
|
|
3691
|
+
const name = String(template?.name || "").trim();
|
|
3692
|
+
if (id) byId.set(id, template);
|
|
3693
|
+
if (name) byName.set(name, template);
|
|
3694
|
+
});
|
|
3695
|
+
return { byId, byName };
|
|
3696
|
+
}, [tmpls]);
|
|
3697
|
+
const workflowGroups = useMemo(() => {
|
|
3698
|
+
return groupItemsByWorkflowCategory(wfs, (wf) => {
|
|
3699
|
+
return (
|
|
3700
|
+
resolveWorkflowTemplateSource(wf, templateLookup.byId, templateLookup.byName)
|
|
3701
|
+
|| { category: wf?.category || "custom" }
|
|
3702
|
+
);
|
|
3703
|
+
});
|
|
3704
|
+
}, [wfs, templateLookup]);
|
|
3705
|
+
const availableTemplateGroups = useMemo(() => {
|
|
3706
|
+
return groupItemsByWorkflowCategory(availableTemplates, (template) => template);
|
|
3707
|
+
}, [availableTemplates]);
|
|
3032
3708
|
|
|
3033
3709
|
return html`
|
|
3034
3710
|
<div style="padding: 0 4px;">
|
|
@@ -3073,127 +3749,144 @@ function WorkflowListView() {
|
|
|
3073
3749
|
<h3 style="font-size: 14px; font-weight: 600; color: var(--color-text-secondary, #8b95a5); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px;">
|
|
3074
3750
|
Your Workflows (${wfs.length})
|
|
3075
3751
|
</h3>
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
<div
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
<
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3752
|
+
${workflowGroups.map((group) => html`
|
|
3753
|
+
<div key=${group.key} style="margin-bottom: 20px;">
|
|
3754
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--color-border, #2a304060);">
|
|
3755
|
+
<span class="icon-inline" style="font-size: 16px;">${resolveIcon(group.icon) || ICONS.dot}</span>
|
|
3756
|
+
<span style="font-size: 13px; font-weight: 600; color: var(--color-text-secondary, #8b95a5);">${group.label}</span>
|
|
3757
|
+
<span style="font-size: 11px; color: var(--color-text-secondary, #6b7280);">(${group.items.length})</span>
|
|
3758
|
+
</div>
|
|
3759
|
+
<div style="display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));">
|
|
3760
|
+
${group.items.map(wf => html`
|
|
3761
|
+
${(() => {
|
|
3762
|
+
const templateState = wf.metadata?.templateState || null;
|
|
3763
|
+
const hasTemplateUpdate = templateState?.updateAvailable === true;
|
|
3764
|
+
const isCustomizedTemplate = templateState?.isCustomized === true;
|
|
3765
|
+
const isCore = wf.core === true;
|
|
3766
|
+
return html`
|
|
3767
|
+
<div key=${wf.id} class="wf-card" style="background: var(--color-bg-secondary, #1a1f2e); border-radius: 12px; padding: 14px; border: 1px solid var(--color-border, #2a3040); cursor: pointer; transition: border-color 0.15s;"
|
|
3768
|
+
onClick=${() => {
|
|
3769
|
+
apiFetch("/api/workflows/" + wf.id).then(d => {
|
|
3770
|
+
activeWorkflow.value = d?.workflow || wf;
|
|
3771
|
+
viewMode.value = "canvas";
|
|
3772
|
+
}).catch(() => { activeWorkflow.value = wf; viewMode.value = "canvas"; });
|
|
3773
|
+
}}>
|
|
3774
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
3775
|
+
<span class="icon-inline" style="font-size: 14px;">${resolveIcon(getNodeMeta(wf.trigger || "action")?.icon) || ICONS.dot}</span>
|
|
3776
|
+
<span style="font-weight: 600; font-size: 14px; flex: 1;">${wf.name}</span>
|
|
3777
|
+
<span class="wf-badge" style="background: ${wf.enabled ? '#10b98130' : '#6b728030'}; color: ${wf.enabled ? '#10b981' : '#6b7280'}; font-size: 10px;">
|
|
3778
|
+
${wf.enabled ? "Active" : "Paused"}
|
|
3779
|
+
</span>
|
|
3780
|
+
${isCore && html`
|
|
3781
|
+
<span class="wf-badge" style="background: #8b5cf620; color: #a78bfa; font-size: 10px; font-weight: 600;">
|
|
3782
|
+
Core
|
|
3783
|
+
</span>
|
|
3784
|
+
`}
|
|
3785
|
+
${templateState?.templateId && html`
|
|
3786
|
+
<span class="wf-badge" style="background: #3b82f620; color: #60a5fa; font-size: 10px;">
|
|
3787
|
+
Template
|
|
3788
|
+
</span>
|
|
3789
|
+
`}
|
|
3790
|
+
${isCustomizedTemplate && html`
|
|
3791
|
+
<span class="wf-badge" style="background: #f59e0b20; color: #f59e0b; font-size: 10px;">
|
|
3792
|
+
Customized
|
|
3793
|
+
</span>
|
|
3794
|
+
`}
|
|
3795
|
+
${hasTemplateUpdate && html`
|
|
3796
|
+
<span class="wf-badge" style="background: #ef444420; color: #f87171; font-size: 10px;">
|
|
3797
|
+
Update Available
|
|
3798
|
+
</span>
|
|
3799
|
+
`}
|
|
3800
|
+
</div>
|
|
3801
|
+
${wf.description && html`
|
|
3802
|
+
<div style="font-size: 12px; color: var(--color-text-secondary, #8b95a5); margin-bottom: 8px; line-height: 1.4;">
|
|
3803
|
+
${wf.description.slice(0, 120)}${wf.description.length > 120 ? "…" : ""}
|
|
3804
|
+
</div>
|
|
3805
|
+
`}
|
|
3806
|
+
${templateState?.templateId && html`
|
|
3807
|
+
<div style="font-size: 11px; color: var(--color-text-secondary, #7f8aa0); margin-bottom: 8px;">
|
|
3808
|
+
${templateState.templateName || templateState.templateId}
|
|
3809
|
+
${templateState.installedTemplateVersion && templateState.templateVersion && templateState.installedTemplateVersion !== templateState.templateVersion && html`
|
|
3810
|
+
<span> · v${templateState.installedTemplateVersion} → v${templateState.templateVersion}</span>
|
|
3811
|
+
`}
|
|
3812
|
+
</div>
|
|
3122
3813
|
`}
|
|
3814
|
+
<div style="display: flex; gap: 8px; align-items: center; font-size: 11px; color: var(--color-text-secondary, #6b7280);">
|
|
3815
|
+
<span>${wf.nodeCount || 0} nodes</span>
|
|
3816
|
+
<span>·</span>
|
|
3817
|
+
<span class="wf-badge" style="font-size: 10px; padding: 2px 8px; background: var(--color-bg, #0d1117); color: var(--color-text-secondary, #8b95a5);">
|
|
3818
|
+
${group.label}
|
|
3819
|
+
</span>
|
|
3820
|
+
<div style="flex: 1;"></div>
|
|
3821
|
+
${hasTemplateUpdate && html`
|
|
3822
|
+
<${Button}
|
|
3823
|
+
variant="text"
|
|
3824
|
+
size="small"
|
|
3825
|
+
sx=${{ fontSize: '11px', borderColor: '#f59e0b80', color: '#f59e0b', textTransform: 'none' }}
|
|
3826
|
+
onClick=${async (e) => {
|
|
3827
|
+
e.stopPropagation();
|
|
3828
|
+
if (!isCustomizedTemplate) {
|
|
3829
|
+
await applyTemplateUpdate(wf.id, "replace", true);
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
const choice = window.prompt(
|
|
3833
|
+
"Template update available for customized workflow.\nType 'copy' to create an updated copy, or 'replace' to overwrite this workflow.",
|
|
3834
|
+
"copy",
|
|
3835
|
+
);
|
|
3836
|
+
const normalized = String(choice || "").trim().toLowerCase();
|
|
3837
|
+
if (normalized === "copy") {
|
|
3838
|
+
await applyTemplateUpdate(wf.id, "copy", false);
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
if (normalized === "replace") {
|
|
3842
|
+
const ok = window.confirm("Replace this customized workflow with latest template? This cannot be undone.");
|
|
3843
|
+
if (!ok) return;
|
|
3844
|
+
await applyTemplateUpdate(wf.id, "replace", true);
|
|
3845
|
+
}
|
|
3846
|
+
}}
|
|
3847
|
+
>
|
|
3848
|
+
<span class="icon-inline">${resolveIcon("refresh")}</span>
|
|
3849
|
+
Update
|
|
3850
|
+
<//>
|
|
3851
|
+
`}
|
|
3852
|
+
${!isCore && html`<${Button}
|
|
3853
|
+
variant="text"
|
|
3854
|
+
size="small"
|
|
3855
|
+
sx=${{ fontSize: '11px', textTransform: 'none' }}
|
|
3856
|
+
onClick=${(e) => {
|
|
3857
|
+
e.stopPropagation();
|
|
3858
|
+
setWorkflowEnabled(wf.id, !wf.enabled);
|
|
3859
|
+
}}
|
|
3860
|
+
>
|
|
3861
|
+
<span class="icon-inline">${resolveIcon(wf.enabled ? "pause" : "play")}</span>
|
|
3862
|
+
${wf.enabled ? "Pause" : "Resume"}
|
|
3863
|
+
<//>`}
|
|
3864
|
+
<${Button}
|
|
3865
|
+
variant="text"
|
|
3866
|
+
size="small"
|
|
3867
|
+
sx=${{ fontSize: '11px', textTransform: 'none', ...(wf.enabled ? {} : { opacity: 0.65 }) }}
|
|
3868
|
+
onClick=${(e) => {
|
|
3869
|
+
e.stopPropagation();
|
|
3870
|
+
if (!wf.enabled) {
|
|
3871
|
+
showToast("Workflow is paused. Resume it before running.", "warning");
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
openExecuteDialog(wf.id);
|
|
3875
|
+
}}
|
|
3876
|
+
>
|
|
3877
|
+
<span class="icon-inline">${resolveIcon("play")}</span>
|
|
3878
|
+
<//>
|
|
3879
|
+
${!isCore && html`<${Button} variant="text" size="small" sx=${{ fontSize: '11px', color: '#ef4444', textTransform: 'none' }} onClick=${(e) => { e.stopPropagation(); if (confirm("Delete " + wf.name + "?")) deleteWorkflow(wf.id); }}>
|
|
3880
|
+
<span class="icon-inline">${resolveIcon("trash")}</span>
|
|
3881
|
+
<//>`}
|
|
3882
|
+
</div>
|
|
3123
3883
|
</div>
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
<span>·</span>
|
|
3128
|
-
<span>${wf.category || "custom"}</span>
|
|
3129
|
-
<div style="flex: 1;"></div>
|
|
3130
|
-
${hasTemplateUpdate && html`
|
|
3131
|
-
<${Button}
|
|
3132
|
-
variant="text"
|
|
3133
|
-
size="small"
|
|
3134
|
-
sx=${{ fontSize: '11px', borderColor: '#f59e0b80', color: '#f59e0b', textTransform: 'none' }}
|
|
3135
|
-
onClick=${async (e) => {
|
|
3136
|
-
e.stopPropagation();
|
|
3137
|
-
if (!isCustomizedTemplate) {
|
|
3138
|
-
await applyTemplateUpdate(wf.id, "replace", true);
|
|
3139
|
-
return;
|
|
3140
|
-
}
|
|
3141
|
-
const choice = window.prompt(
|
|
3142
|
-
"Template update available for customized workflow.\nType 'copy' to create an updated copy, or 'replace' to overwrite this workflow.",
|
|
3143
|
-
"copy",
|
|
3144
|
-
);
|
|
3145
|
-
const normalized = String(choice || "").trim().toLowerCase();
|
|
3146
|
-
if (normalized === "copy") {
|
|
3147
|
-
await applyTemplateUpdate(wf.id, "copy", false);
|
|
3148
|
-
return;
|
|
3149
|
-
}
|
|
3150
|
-
if (normalized === "replace") {
|
|
3151
|
-
const ok = window.confirm("Replace this customized workflow with latest template? This cannot be undone.");
|
|
3152
|
-
if (!ok) return;
|
|
3153
|
-
await applyTemplateUpdate(wf.id, "replace", true);
|
|
3154
|
-
}
|
|
3155
|
-
}}
|
|
3156
|
-
>
|
|
3157
|
-
<span class="icon-inline">${resolveIcon("refresh")}</span>
|
|
3158
|
-
Update
|
|
3159
|
-
<//>
|
|
3160
|
-
`}
|
|
3161
|
-
<${Button}
|
|
3162
|
-
variant="text"
|
|
3163
|
-
size="small"
|
|
3164
|
-
sx=${{ fontSize: '11px', textTransform: 'none' }}
|
|
3165
|
-
onClick=${(e) => {
|
|
3166
|
-
e.stopPropagation();
|
|
3167
|
-
setWorkflowEnabled(wf.id, !wf.enabled);
|
|
3168
|
-
}}
|
|
3169
|
-
>
|
|
3170
|
-
<span class="icon-inline">${resolveIcon(wf.enabled ? "pause" : "play")}</span>
|
|
3171
|
-
${wf.enabled ? "Pause" : "Resume"}
|
|
3172
|
-
<//>
|
|
3173
|
-
<${Button}
|
|
3174
|
-
variant="text"
|
|
3175
|
-
size="small"
|
|
3176
|
-
sx=${{ fontSize: '11px', textTransform: 'none', ...(wf.enabled ? {} : { opacity: 0.65 }) }}
|
|
3177
|
-
onClick=${(e) => {
|
|
3178
|
-
e.stopPropagation();
|
|
3179
|
-
if (!wf.enabled) {
|
|
3180
|
-
showToast("Workflow is paused. Resume it before running.", "warning");
|
|
3181
|
-
return;
|
|
3182
|
-
}
|
|
3183
|
-
openExecuteDialog(wf.id);
|
|
3184
|
-
}}
|
|
3185
|
-
>
|
|
3186
|
-
<span class="icon-inline">${resolveIcon("play")}</span>
|
|
3187
|
-
<//>
|
|
3188
|
-
<${Button} variant="text" size="small" sx=${{ fontSize: '11px', color: '#ef4444', textTransform: 'none' }} onClick=${(e) => { e.stopPropagation(); if (confirm("Delete " + wf.name + "?")) deleteWorkflow(wf.id); }}>
|
|
3189
|
-
<span class="icon-inline">${resolveIcon("trash")}</span>
|
|
3190
|
-
<//>
|
|
3191
|
-
</div>
|
|
3884
|
+
`;
|
|
3885
|
+
})()}
|
|
3886
|
+
`)}
|
|
3192
3887
|
</div>
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
`)}
|
|
3196
|
-
</div>
|
|
3888
|
+
</div>
|
|
3889
|
+
`)}
|
|
3197
3890
|
</div>
|
|
3198
3891
|
`}
|
|
3199
3892
|
|
|
@@ -3233,60 +3926,50 @@ function WorkflowListView() {
|
|
|
3233
3926
|
<span>All templates are installed!</span>
|
|
3234
3927
|
</div>
|
|
3235
3928
|
`}
|
|
3236
|
-
${(() =>
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
3256
|
-
<span class="icon-inline" style="font-size: 14px;">${resolveIcon(t.categoryIcon || group.icon) || ICONS.dot}</span>
|
|
3257
|
-
<span style="font-weight: 600; font-size: 14px; flex: 1;">${t.name}</span>
|
|
3258
|
-
${t.recommended && html`
|
|
3259
|
-
<span class="wf-badge" style="background: #10b98125; color: #10b981; border-color: #10b98140; font-size: 10px; padding: 2px 8px; font-weight: 600; letter-spacing: 0.3px; display: inline-flex; align-items: center; gap: 4px;">
|
|
3260
|
-
<span class="icon-inline">${resolveIcon("star")}</span>
|
|
3261
|
-
Recommended
|
|
3262
|
-
</span>
|
|
3263
|
-
`}
|
|
3264
|
-
</div>
|
|
3265
|
-
<div style="font-size: 12px; color: var(--color-text-secondary, #8b95a5); margin-bottom: 10px; line-height: 1.4;">
|
|
3266
|
-
${t.description?.slice(0, 120)}${(t.description?.length || 0) > 120 ? "…" : ""}
|
|
3267
|
-
</div>
|
|
3268
|
-
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;">
|
|
3269
|
-
${(t.tags || []).map(tag => html`
|
|
3270
|
-
<span key=${tag} class="wf-badge" style="font-size: 10px; padding: 2px 6px;">${tag}</span>
|
|
3271
|
-
`)}
|
|
3272
|
-
</div>
|
|
3273
|
-
<div style="display: flex; gap: 8px; align-items: center;">
|
|
3274
|
-
<span style="font-size: 11px; color: var(--color-text-secondary, #6b7280);">${t.nodeCount} nodes</span>
|
|
3275
|
-
<div style="flex: 1;"></div>
|
|
3276
|
-
<${Button}
|
|
3277
|
-
variant="contained"
|
|
3278
|
-
size="small"
|
|
3279
|
-
onClick=${() => openInstallTemplateDialog(t.id)}
|
|
3280
|
-
>
|
|
3281
|
-
Install →
|
|
3282
|
-
<//>
|
|
3283
|
-
</div>
|
|
3929
|
+
${availableTemplateGroups.map((group) => html`
|
|
3930
|
+
<div key=${group.key} style="margin-bottom: 20px;">
|
|
3931
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--color-border, #2a304060);">
|
|
3932
|
+
<span class="icon-inline" style="font-size: 16px;">${resolveIcon(group.icon) || ICONS.dot}</span>
|
|
3933
|
+
<span style="font-size: 13px; font-weight: 600; color: var(--color-text-secondary, #8b95a5);">${group.label}</span>
|
|
3934
|
+
<span style="font-size: 11px; color: var(--color-text-secondary, #6b7280);">(${group.items.length})</span>
|
|
3935
|
+
</div>
|
|
3936
|
+
<div style="display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));">
|
|
3937
|
+
${group.items.map(t => html`
|
|
3938
|
+
<div key=${t.id} class="wf-card wf-template-card" style="background: var(--color-bg-secondary, #1a1f2e); border-radius: 12px; padding: 14px; border: 1px solid var(--color-border, #2a304080);">
|
|
3939
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
3940
|
+
<span class="icon-inline" style="font-size: 14px;">${resolveIcon(t.categoryIcon || group.icon) || ICONS.dot}</span>
|
|
3941
|
+
<span style="font-weight: 600; font-size: 14px; flex: 1;">${t.name}</span>
|
|
3942
|
+
${t.recommended && html`
|
|
3943
|
+
<span class="wf-badge" style="background: #10b98125; color: #10b981; border-color: #10b98140; font-size: 10px; padding: 2px 8px; font-weight: 600; letter-spacing: 0.3px; display: inline-flex; align-items: center; gap: 4px;">
|
|
3944
|
+
<span class="icon-inline">${resolveIcon("star")}</span>
|
|
3945
|
+
Recommended
|
|
3946
|
+
</span>
|
|
3947
|
+
`}
|
|
3284
3948
|
</div>
|
|
3285
|
-
|
|
3286
|
-
|
|
3949
|
+
<div style="font-size: 12px; color: var(--color-text-secondary, #8b95a5); margin-bottom: 10px; line-height: 1.4;">
|
|
3950
|
+
${t.description?.slice(0, 120)}${(t.description?.length || 0) > 120 ? "…" : ""}
|
|
3951
|
+
</div>
|
|
3952
|
+
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;">
|
|
3953
|
+
${(t.tags || []).map(tag => html`
|
|
3954
|
+
<span key=${tag} class="wf-badge" style="font-size: 10px; padding: 2px 6px;">${tag}</span>
|
|
3955
|
+
`)}
|
|
3956
|
+
</div>
|
|
3957
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
3958
|
+
<span style="font-size: 11px; color: var(--color-text-secondary, #6b7280);">${t.nodeCount} nodes</span>
|
|
3959
|
+
<div style="flex: 1;"></div>
|
|
3960
|
+
<${Button}
|
|
3961
|
+
variant="contained"
|
|
3962
|
+
size="small"
|
|
3963
|
+
onClick=${() => openInstallTemplateDialog(t.id)}
|
|
3964
|
+
>
|
|
3965
|
+
Install →
|
|
3966
|
+
<//>
|
|
3967
|
+
</div>
|
|
3968
|
+
</div>
|
|
3969
|
+
`)}
|
|
3287
3970
|
</div>
|
|
3288
|
-
|
|
3289
|
-
|
|
3971
|
+
</div>
|
|
3972
|
+
`)}
|
|
3290
3973
|
</div>
|
|
3291
3974
|
</div>
|
|
3292
3975
|
`;
|
|
@@ -3297,19 +3980,22 @@ function WorkflowListView() {
|
|
|
3297
3980
|
* ═══════════════════════════════════════════════════════════════ */
|
|
3298
3981
|
|
|
3299
3982
|
function getRunStatusBadgeStyles(status) {
|
|
3300
|
-
|
|
3301
|
-
if (
|
|
3302
|
-
if (
|
|
3983
|
+
const normalized = normalizeLiveNodeStatus(status) || String(status || "").trim().toLowerCase();
|
|
3984
|
+
if (normalized === "completed" || normalized === "success") return { bg: "#10b98130", color: "#10b981" };
|
|
3985
|
+
if (normalized === "failed" || normalized === "fail") return { bg: "#ef444430", color: "#ef4444" };
|
|
3986
|
+
if (normalized === "running") return { bg: "#3b82f630", color: "#60a5fa" };
|
|
3987
|
+
if (normalized === "skipped") return { bg: "#94a3b830", color: "#94a3b8" };
|
|
3303
3988
|
return { bg: "#6b728030", color: "#9ca3af" };
|
|
3304
3989
|
}
|
|
3305
3990
|
|
|
3306
3991
|
function getNodeStatusRank(status) {
|
|
3307
|
-
|
|
3308
|
-
if (
|
|
3992
|
+
const normalized = normalizeLiveNodeStatus(status) || status;
|
|
3993
|
+
if (normalized === "running") return 0;
|
|
3994
|
+
if (normalized === "failed" || normalized === "fail") return 1;
|
|
3309
3995
|
if (status === "waiting") return 2;
|
|
3310
3996
|
if (status === "pending") return 3;
|
|
3311
|
-
if (
|
|
3312
|
-
if (
|
|
3997
|
+
if (normalized === "completed" || normalized === "success") return 4;
|
|
3998
|
+
if (normalized === "skipped") return 5;
|
|
3313
3999
|
return 6;
|
|
3314
4000
|
}
|
|
3315
4001
|
|
|
@@ -3367,36 +4053,6 @@ function getWorkflowRunTriggerLabel(run) {
|
|
|
3367
4053
|
return rawSource || normalizedSource || "unknown";
|
|
3368
4054
|
}
|
|
3369
4055
|
|
|
3370
|
-
function buildNodeStatusesFromRunDetail(run) {
|
|
3371
|
-
const detail = run?.detail || {};
|
|
3372
|
-
const statuses = { ...(detail?.nodeStatuses || {}) };
|
|
3373
|
-
const statusEvents = Array.isArray(detail?.nodeStatusEvents) ? detail.nodeStatusEvents : [];
|
|
3374
|
-
const logs = Array.isArray(detail?.logs) ? detail.logs : [];
|
|
3375
|
-
|
|
3376
|
-
for (const event of statusEvents) {
|
|
3377
|
-
const nodeId = String(event?.nodeId || "").trim();
|
|
3378
|
-
const status = String(event?.status || "").trim();
|
|
3379
|
-
if (!nodeId || !status) continue;
|
|
3380
|
-
statuses[nodeId] = status;
|
|
3381
|
-
}
|
|
3382
|
-
|
|
3383
|
-
// Backfill older runs that only recorded nodeId in logs.
|
|
3384
|
-
if (Object.keys(statuses).length === 0) {
|
|
3385
|
-
const fallbackStatus = run?.status === "failed"
|
|
3386
|
-
? "failed"
|
|
3387
|
-
: run?.status === "completed"
|
|
3388
|
-
? "completed"
|
|
3389
|
-
: "running";
|
|
3390
|
-
for (const entry of logs) {
|
|
3391
|
-
const nodeId = String(entry?.nodeId || "").trim();
|
|
3392
|
-
if (!nodeId || statuses[nodeId]) continue;
|
|
3393
|
-
statuses[nodeId] = fallbackStatus;
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
return statuses;
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
4056
|
function getNodeCardBorder(status) {
|
|
3401
4057
|
if (status === "running") return "#3b82f680";
|
|
3402
4058
|
if (status === "failed") return "#ef444480";
|
|
@@ -3426,9 +4082,6 @@ function RunHistoryView() {
|
|
|
3426
4082
|
const [nowTick, setNowTick] = useState(Date.now());
|
|
3427
4083
|
const hasRunningRuns = runs.some((run) => run?.status === "running");
|
|
3428
4084
|
const selectedRunIsRunning = selectedRun?.status === "running";
|
|
3429
|
-
const autoLoadMoreRef = useRef(false);
|
|
3430
|
-
const tailSentinelRef = useRef(null);
|
|
3431
|
-
const lastAutoLoadCountRef = useRef(-1);
|
|
3432
4085
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
3433
4086
|
const [workflowFilter, setWorkflowFilter] = useState("all");
|
|
3434
4087
|
const [triggerFilter, setTriggerFilter] = useState("all");
|
|
@@ -3513,12 +4166,6 @@ function RunHistoryView() {
|
|
|
3513
4166
|
|
|
3514
4167
|
const canLoadMoreRuns =
|
|
3515
4168
|
hasMoreRuns && runs.length < WORKFLOW_RUN_MAX_FETCH;
|
|
3516
|
-
const hasRunFilters =
|
|
3517
|
-
statusFilter !== "all" ||
|
|
3518
|
-
workflowFilter !== "all" ||
|
|
3519
|
-
triggerFilter !== "all" ||
|
|
3520
|
-
Boolean(normalizedSearch);
|
|
3521
|
-
|
|
3522
4169
|
const triggerLoadMoreRuns = useCallback(() => {
|
|
3523
4170
|
if (!canLoadMoreRuns || loadingMoreRuns) return false;
|
|
3524
4171
|
const nextOffset = Number(workflowRunsNextOffset.value || runs.length);
|
|
@@ -3531,61 +4178,6 @@ function RunHistoryView() {
|
|
|
3531
4178
|
return true;
|
|
3532
4179
|
}, [canLoadMoreRuns, loadingMoreRuns, runs.length, totalRuns]);
|
|
3533
4180
|
|
|
3534
|
-
useEffect(() => {
|
|
3535
|
-
if (!hasRunFilters || filteredRuns.length > 0 || !canLoadMoreRuns) {
|
|
3536
|
-
autoLoadMoreRef.current = false;
|
|
3537
|
-
return;
|
|
3538
|
-
}
|
|
3539
|
-
if (autoLoadMoreRef.current) return;
|
|
3540
|
-
autoLoadMoreRef.current = true;
|
|
3541
|
-
Promise.resolve(triggerLoadMoreRuns())
|
|
3542
|
-
.finally(() => {
|
|
3543
|
-
autoLoadMoreRef.current = false;
|
|
3544
|
-
});
|
|
3545
|
-
}, [hasRunFilters, filteredRuns.length, canLoadMoreRuns, triggerLoadMoreRuns, statusFilter, workflowFilter, triggerFilter, normalizedSearch]);
|
|
3546
|
-
|
|
3547
|
-
useEffect(() => {
|
|
3548
|
-
if (selectedRun || !canLoadMoreRuns || typeof IntersectionObserver !== "function") {
|
|
3549
|
-
lastAutoLoadCountRef.current = -1;
|
|
3550
|
-
return;
|
|
3551
|
-
}
|
|
3552
|
-
const sentinel = tailSentinelRef.current;
|
|
3553
|
-
if (!sentinel) return;
|
|
3554
|
-
const observer = new IntersectionObserver(
|
|
3555
|
-
(entries) => {
|
|
3556
|
-
for (const entry of entries) {
|
|
3557
|
-
if (!entry.isIntersecting) continue;
|
|
3558
|
-
const key = runs.length;
|
|
3559
|
-
if (lastAutoLoadCountRef.current === key || loadingMoreRuns) continue;
|
|
3560
|
-
lastAutoLoadCountRef.current = key;
|
|
3561
|
-
triggerLoadMoreRuns();
|
|
3562
|
-
}
|
|
3563
|
-
},
|
|
3564
|
-
{
|
|
3565
|
-
root: null,
|
|
3566
|
-
rootMargin: "0px 0px 240px 0px",
|
|
3567
|
-
threshold: 0,
|
|
3568
|
-
},
|
|
3569
|
-
);
|
|
3570
|
-
observer.observe(sentinel);
|
|
3571
|
-
return () => observer.disconnect();
|
|
3572
|
-
}, [selectedRun, canLoadMoreRuns, loadingMoreRuns, runs.length, triggerLoadMoreRuns]);
|
|
3573
|
-
|
|
3574
|
-
useEffect(() => {
|
|
3575
|
-
if (selectedRun || !canLoadMoreRuns || loadingMoreRuns || typeof window === "undefined") return;
|
|
3576
|
-
const doc = document?.documentElement;
|
|
3577
|
-
const body = document?.body;
|
|
3578
|
-
const scrollHeight = Math.max(
|
|
3579
|
-
Number(doc?.scrollHeight || 0),
|
|
3580
|
-
Number(body?.scrollHeight || 0),
|
|
3581
|
-
);
|
|
3582
|
-
if (scrollHeight > window.innerHeight + 240) return;
|
|
3583
|
-
const key = runs.length;
|
|
3584
|
-
if (lastAutoLoadCountRef.current === key) return;
|
|
3585
|
-
lastAutoLoadCountRef.current = key;
|
|
3586
|
-
triggerLoadMoreRuns();
|
|
3587
|
-
}, [selectedRun, canLoadMoreRuns, loadingMoreRuns, runs.length, filteredRuns.length, triggerLoadMoreRuns]);
|
|
3588
|
-
|
|
3589
4181
|
if (selectedRun) {
|
|
3590
4182
|
const statusStyles = getRunStatusBadgeStyles(selectedRun.status);
|
|
3591
4183
|
const logs = Array.isArray(selectedRun?.detail?.logs) ? selectedRun.detail.logs : [];
|
|
@@ -3787,7 +4379,7 @@ function RunHistoryView() {
|
|
|
3787
4379
|
<div style="text-align: center; padding: 28px; opacity: 0.6;">
|
|
3788
4380
|
<div>No runs match the current filters yet.</div>
|
|
3789
4381
|
${canLoadMoreRuns && html`
|
|
3790
|
-
<div style="margin-top: 6px;">Bosun has loaded ${runs.length} of ${totalRuns} run(s);
|
|
4382
|
+
<div style="margin-top: 6px;">Bosun has loaded ${runs.length} of ${totalRuns} run(s); use Load more runs to search older history.</div>
|
|
3791
4383
|
`}
|
|
3792
4384
|
</div>
|
|
3793
4385
|
`}
|
|
@@ -3842,9 +4434,6 @@ function RunHistoryView() {
|
|
|
3842
4434
|
<//>
|
|
3843
4435
|
`;
|
|
3844
4436
|
})}
|
|
3845
|
-
${canLoadMoreRuns && html`
|
|
3846
|
-
<div ref=${tailSentinelRef} style="height: 1px;"></div>
|
|
3847
|
-
`}
|
|
3848
4437
|
</div>
|
|
3849
4438
|
${canLoadMoreRuns && html`
|
|
3850
4439
|
<div style="display: flex; justify-content: center; margin-top: 12px;">
|
|
@@ -4094,6 +4683,34 @@ export function WorkflowsTab() {
|
|
|
4094
4683
|
.wf-preset-btn:hover { border-color: #3b82f6 !important; background: var(--bg-card-hover) !important; }
|
|
4095
4684
|
.wf-preset-section { animation: wf-fade-in 0.15s ease; }
|
|
4096
4685
|
@keyframes wf-fade-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
|
|
4686
|
+
.wf-node-running rect:first-child {
|
|
4687
|
+
animation: wf-node-running-pulse 1.1s ease-in-out infinite;
|
|
4688
|
+
}
|
|
4689
|
+
.wf-node-flash-success rect:first-child {
|
|
4690
|
+
animation: wf-node-flash-success 0.45s ease-in-out 2;
|
|
4691
|
+
}
|
|
4692
|
+
.wf-node-flash-fail rect:first-child {
|
|
4693
|
+
animation: wf-node-flash-fail 0.45s ease-in-out 2;
|
|
4694
|
+
}
|
|
4695
|
+
.wf-node-flash-skipped rect:first-child {
|
|
4696
|
+
animation: wf-node-flash-skipped 0.45s ease-in-out 2;
|
|
4697
|
+
}
|
|
4698
|
+
@keyframes wf-node-running-pulse {
|
|
4699
|
+
0%, 100% { stroke-opacity: 0.6; }
|
|
4700
|
+
50% { stroke-opacity: 1; }
|
|
4701
|
+
}
|
|
4702
|
+
@keyframes wf-node-flash-success {
|
|
4703
|
+
0%, 100% { filter: none; }
|
|
4704
|
+
50% { filter: drop-shadow(0 0 10px rgba(16, 185, 129, 0.65)); }
|
|
4705
|
+
}
|
|
4706
|
+
@keyframes wf-node-flash-fail {
|
|
4707
|
+
0%, 100% { filter: none; }
|
|
4708
|
+
50% { filter: drop-shadow(0 0 10px rgba(239, 68, 68, 0.65)); }
|
|
4709
|
+
}
|
|
4710
|
+
@keyframes wf-node-flash-skipped {
|
|
4711
|
+
0%, 100% { filter: none; }
|
|
4712
|
+
50% { filter: drop-shadow(0 0 8px rgba(148, 163, 184, 0.55)); }
|
|
4713
|
+
}
|
|
4097
4714
|
.wf-canvas-container { height: calc(100vh - 140px); min-height: 500px; }
|
|
4098
4715
|
@media (min-width: 1200px) { .wf-canvas-container { height: calc(100vh - 120px); min-height: 700px; } }
|
|
4099
4716
|
</style>
|
|
@@ -4113,4 +4730,3 @@ export function WorkflowsTab() {
|
|
|
4113
4730
|
</div>
|
|
4114
4731
|
`;
|
|
4115
4732
|
}
|
|
4116
|
-
|