bosun 0.41.2 → 0.41.3
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/.env.example +1 -1
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +6 -3
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +28 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/ui-server.mjs +1194 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +21 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +334 -80
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +21 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +785 -140
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +304 -52
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +20 -9
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/ui/tabs/workflows.js
CHANGED
|
@@ -19,11 +19,13 @@ import { formatDate, formatDuration, formatRelative } from "../modules/utils.js"
|
|
|
19
19
|
import {
|
|
20
20
|
HISTORY_LIMIT,
|
|
21
21
|
HISTORY_COMMIT_DEBOUNCE_MS,
|
|
22
|
+
buildNodeStatusesFromRunDetail,
|
|
22
23
|
createHistoryState,
|
|
23
24
|
getNodeSearchMetadata,
|
|
24
25
|
parseGraphSnapshot,
|
|
25
26
|
pushHistorySnapshot,
|
|
26
27
|
redoHistory,
|
|
28
|
+
resolveNodeOutputPreview,
|
|
27
29
|
searchNodeTypes,
|
|
28
30
|
serializeGraphSnapshot,
|
|
29
31
|
undoHistory,
|
|
@@ -67,6 +69,9 @@ const WORKFLOW_LIVE_WS_BATCH_MS = 90;
|
|
|
67
69
|
const NODE_COMPLETION_FLASH_MS = 1400;
|
|
68
70
|
const NODE_RUNNING_HINT_MS = 500;
|
|
69
71
|
const EDGE_FLOW_ANIMATION_MS = 1200;
|
|
72
|
+
const WORKFLOW_NODE_HEADER_HEIGHT = 44;
|
|
73
|
+
const NODE_HEADER = WORKFLOW_NODE_HEADER_HEIGHT;
|
|
74
|
+
const NODE_HEADER_H = WORKFLOW_NODE_HEADER_HEIGHT;
|
|
70
75
|
const workflowRunsLimit = signal(WORKFLOW_RUN_PAGE_SIZE);
|
|
71
76
|
|
|
72
77
|
// ── Execute Dialog state ──────────────────────────────────────────────────
|
|
@@ -85,6 +90,15 @@ const installDialogVars = signal({});
|
|
|
85
90
|
const installDialogMode = signal("quick");
|
|
86
91
|
const installDialogInstalling = signal(false);
|
|
87
92
|
const installDialogResult = signal(null);
|
|
93
|
+
const workflowsLoading = signal(false);
|
|
94
|
+
const templatesLoading = signal(false);
|
|
95
|
+
const nodeTypesLoading = signal(false);
|
|
96
|
+
|
|
97
|
+
function getWorkflowNameById(workflowId) {
|
|
98
|
+
const id = String(workflowId || "").trim();
|
|
99
|
+
if (!id) return "";
|
|
100
|
+
return (workflows.value || []).find((workflow) => workflow?.id === id)?.name || id;
|
|
101
|
+
}
|
|
88
102
|
|
|
89
103
|
function resetWorkflowRunsState(scopeWorkflowId = null) {
|
|
90
104
|
workflowRuns.value = [];
|
|
@@ -127,34 +141,78 @@ function returnToWorkflowList() {
|
|
|
127
141
|
setRouteParams({}, { replace: true, skipGuard: true });
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
function openWorkflowCanvas(workflowId) {
|
|
145
|
+
const id = String(workflowId || "").trim();
|
|
146
|
+
if (!id) return;
|
|
147
|
+
apiFetch(`/api/workflows/${encodeURIComponent(id)}`)
|
|
148
|
+
.then((data) => {
|
|
149
|
+
activeWorkflow.value = data?.workflow || (workflows.value || []).find((workflow) => workflow.id === id) || null;
|
|
150
|
+
if (activeWorkflow.value) {
|
|
151
|
+
viewMode.value = "canvas";
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
.catch(() => {
|
|
155
|
+
const existing = (workflows.value || []).find((workflow) => workflow.id === id) || null;
|
|
156
|
+
if (existing) {
|
|
157
|
+
activeWorkflow.value = existing;
|
|
158
|
+
viewMode.value = "canvas";
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function openWorkflowRunsView(workflowId, runId = null) {
|
|
164
|
+
const scopedWorkflowId = String(workflowId || "").trim() || null;
|
|
165
|
+
resetWorkflowRunsState(scopedWorkflowId);
|
|
166
|
+
selectedRunId.value = null;
|
|
167
|
+
selectedRunDetail.value = null;
|
|
168
|
+
viewMode.value = "runs";
|
|
169
|
+
const route = { runsView: true };
|
|
170
|
+
if (scopedWorkflowId) route.runsWorkflowId = scopedWorkflowId;
|
|
171
|
+
if (runId) route.runId = runId;
|
|
172
|
+
setRouteParams(route, { replace: false, skipGuard: true });
|
|
173
|
+
loadRuns(scopedWorkflowId, { reset: true }).catch(() => {});
|
|
174
|
+
if (runId) {
|
|
175
|
+
loadRunDetail(runId, { workflowId: scopedWorkflowId }).catch(() => {});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
130
179
|
/* ═══════════════════════════════════════════════════════════════
|
|
131
180
|
* API Helpers
|
|
132
181
|
* ═══════════════════════════════════════════════════════════════ */
|
|
133
182
|
|
|
134
183
|
async function loadWorkflows() {
|
|
184
|
+
workflowsLoading.value = true;
|
|
135
185
|
try {
|
|
136
186
|
const data = await apiFetch("/api/workflows");
|
|
137
187
|
if (data?.workflows) workflows.value = data.workflows;
|
|
138
188
|
} catch (err) {
|
|
139
189
|
console.error("[workflows] Failed to load:", err);
|
|
190
|
+
} finally {
|
|
191
|
+
workflowsLoading.value = false;
|
|
140
192
|
}
|
|
141
193
|
}
|
|
142
194
|
|
|
143
195
|
async function loadTemplates() {
|
|
196
|
+
templatesLoading.value = true;
|
|
144
197
|
try {
|
|
145
198
|
const data = await apiFetch("/api/workflows/templates");
|
|
146
199
|
if (data?.templates) templates.value = data.templates;
|
|
147
200
|
} catch (err) {
|
|
148
201
|
console.error("[workflows] Failed to load templates:", err);
|
|
202
|
+
} finally {
|
|
203
|
+
templatesLoading.value = false;
|
|
149
204
|
}
|
|
150
205
|
}
|
|
151
206
|
|
|
152
207
|
async function loadNodeTypes() {
|
|
208
|
+
nodeTypesLoading.value = true;
|
|
153
209
|
try {
|
|
154
210
|
const data = await apiFetch("/api/workflows/node-types");
|
|
155
211
|
if (data?.nodeTypes) nodeTypes.value = data.nodeTypes;
|
|
156
212
|
} catch (err) {
|
|
157
213
|
console.error("[workflows] Failed to load node types:", err);
|
|
214
|
+
} finally {
|
|
215
|
+
nodeTypesLoading.value = false;
|
|
158
216
|
}
|
|
159
217
|
}
|
|
160
218
|
|
|
@@ -602,7 +660,7 @@ function ExecuteWorkflowDialog() {
|
|
|
602
660
|
PaperProps=${{ sx: { bgcolor: 'var(--color-bg-secondary, #1a1f2e)', color: 'var(--color-text, #e8eaf0)', borderRadius: '12px' } }}
|
|
603
661
|
>
|
|
604
662
|
<${DialogTitle} sx=${{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
605
|
-
<span
|
|
663
|
+
<span class="icon-inline">${resolveIcon("play")}</span>
|
|
606
664
|
<span>Execute: ${wf.name}</span>
|
|
607
665
|
<//>
|
|
608
666
|
|
|
@@ -962,7 +1020,7 @@ function InstallTemplateDialog() {
|
|
|
962
1020
|
PaperProps=${{ sx: { bgcolor: 'var(--color-bg-secondary, #1a1f2e)', color: 'var(--color-text, #e8eaf0)', borderRadius: '12px' } }}
|
|
963
1021
|
>
|
|
964
1022
|
<${DialogTitle} sx=${{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
965
|
-
<span
|
|
1023
|
+
<span class="icon-inline">${resolveIcon("download")}</span>
|
|
966
1024
|
<span>Install: ${template.name}</span>
|
|
967
1025
|
<//>
|
|
968
1026
|
|
|
@@ -1141,6 +1199,55 @@ async function applyTemplateUpdate(workflowId, mode = "replace", force = false)
|
|
|
1141
1199
|
return null;
|
|
1142
1200
|
}
|
|
1143
1201
|
|
|
1202
|
+
async function relayoutTemplateWorkflow(workflowId) {
|
|
1203
|
+
try {
|
|
1204
|
+
const data = await apiFetch(`/api/workflows/${encodeURIComponent(workflowId)}/reflow-layout`, {
|
|
1205
|
+
method: "POST",
|
|
1206
|
+
headers: { "Content-Type": "application/json" },
|
|
1207
|
+
body: JSON.stringify({ workflowId }),
|
|
1208
|
+
});
|
|
1209
|
+
if (data?.workflow) {
|
|
1210
|
+
const refreshed = data.workflow;
|
|
1211
|
+
workflows.value = (workflows.value || []).map((workflow) => (
|
|
1212
|
+
workflow.id === refreshed.id ? refreshed : workflow
|
|
1213
|
+
));
|
|
1214
|
+
if (activeWorkflow.value?.id === refreshed.id) {
|
|
1215
|
+
activeWorkflow.value = refreshed;
|
|
1216
|
+
}
|
|
1217
|
+
showToast("Workflow layout refreshed", "success");
|
|
1218
|
+
return refreshed;
|
|
1219
|
+
}
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
showToast(`Failed to refresh workflow layout: ${err.message}`, "error");
|
|
1222
|
+
}
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function relayoutInstalledTemplateWorkflows() {
|
|
1227
|
+
try {
|
|
1228
|
+
const data = await apiFetch("/api/workflows/reflow-template-layouts", {
|
|
1229
|
+
method: "POST",
|
|
1230
|
+
headers: { "Content-Type": "application/json" },
|
|
1231
|
+
body: JSON.stringify({}),
|
|
1232
|
+
});
|
|
1233
|
+
const result = data?.result || {};
|
|
1234
|
+
const updated = Number(result.updated || 0);
|
|
1235
|
+
await loadWorkflows();
|
|
1236
|
+
if (activeWorkflow.value?.id) {
|
|
1237
|
+
const updatedActive = (workflows.value || []).find((workflow) => workflow.id === activeWorkflow.value.id);
|
|
1238
|
+
if (updatedActive) activeWorkflow.value = updatedActive;
|
|
1239
|
+
}
|
|
1240
|
+
showToast(
|
|
1241
|
+
updated > 0 ? `Refreshed layout for ${updated} template workflow${updated === 1 ? "" : "s"}` : "No template workflows needed relayout",
|
|
1242
|
+
"success",
|
|
1243
|
+
);
|
|
1244
|
+
return result;
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
showToast(`Failed to refresh template layouts: ${err.message}`, "error");
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1144
1251
|
async function loadRuns(workflowId, opts = {}) {
|
|
1145
1252
|
const append = opts.append === true;
|
|
1146
1253
|
const hasScopedWorkflowId = workflowId !== undefined;
|
|
@@ -1194,15 +1301,21 @@ async function loadRuns(workflowId, opts = {}) {
|
|
|
1194
1301
|
}
|
|
1195
1302
|
}
|
|
1196
1303
|
|
|
1197
|
-
async function loadRunDetail(runId) {
|
|
1304
|
+
async function loadRunDetail(runId, opts = {}) {
|
|
1198
1305
|
if (!runId) return;
|
|
1199
1306
|
try {
|
|
1200
1307
|
const data = await apiFetch(`/api/workflows/runs/${encodeURIComponent(runId)}`);
|
|
1201
1308
|
if (data?.run) {
|
|
1309
|
+
const scopedWorkflowId = String(opts?.workflowId || workflowRunsScopeId.value || data.run.workflowId || "").trim() || null;
|
|
1310
|
+
if (scopedWorkflowId) {
|
|
1311
|
+
workflowRunsScopeId.value = scopedWorkflowId;
|
|
1312
|
+
}
|
|
1202
1313
|
selectedRunId.value = runId;
|
|
1203
1314
|
selectedRunDetail.value = data.run;
|
|
1204
1315
|
viewMode.value = "runs";
|
|
1205
|
-
|
|
1316
|
+
const route = { runsView: true, runId };
|
|
1317
|
+
if (scopedWorkflowId) route.runsWorkflowId = scopedWorkflowId;
|
|
1318
|
+
setRouteParams(route, { replace: false, skipGuard: true });
|
|
1206
1319
|
}
|
|
1207
1320
|
} catch (err) {
|
|
1208
1321
|
showToast("Failed to load run details", "error");
|
|
@@ -1402,6 +1515,10 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1402
1515
|
const [marquee, setMarquee] = useState(null);
|
|
1403
1516
|
const [liveHighlightEnabled, setLiveHighlightEnabled] = useState(true);
|
|
1404
1517
|
const [liveRun, setLiveRun] = useState(null);
|
|
1518
|
+
const [recentRuns, setRecentRuns] = useState([]);
|
|
1519
|
+
const [recentRunsTotal, setRecentRunsTotal] = useState(0);
|
|
1520
|
+
const [recentRunsLoading, setRecentRunsLoading] = useState(false);
|
|
1521
|
+
const [runsPanelOpen, setRunsPanelOpen] = useState(true);
|
|
1405
1522
|
const [liveNodeStatuses, setLiveNodeStatuses] = useState({});
|
|
1406
1523
|
const [liveNodeOutputPreviews, setLiveNodeOutputPreviews] = useState({});
|
|
1407
1524
|
const [liveNodeFlashStates, setLiveNodeFlashStates] = useState({});
|
|
@@ -1419,6 +1536,14 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1419
1536
|
const selectedNodeIdsRef = useRef(selectedNodeIds);
|
|
1420
1537
|
const liveEventQueueRef = useRef([]);
|
|
1421
1538
|
const liveEventFlushTimerRef = useRef(null);
|
|
1539
|
+
// Derived live status helpers
|
|
1540
|
+
const hasLiveStatuses = Object.keys(liveNodeStatuses).length > 0;
|
|
1541
|
+
const liveActiveNodes = Object.values(liveNodeStatuses).filter(
|
|
1542
|
+
(s) => s === "running" || s === "active" || s === "in_progress",
|
|
1543
|
+
).length;
|
|
1544
|
+
const liveRunDuration = liveRun?.status === "running" && liveRun?.startedAt
|
|
1545
|
+
? Math.max(0, liveNowTick - Number(liveRun.startedAt))
|
|
1546
|
+
: Number(liveRun?.duration) || 0;
|
|
1422
1547
|
const workflowSnapshotKey = useMemo(
|
|
1423
1548
|
() => serializeGraphSnapshot(workflow?.nodes || [], workflow?.edges || []),
|
|
1424
1549
|
[workflow?.nodes, workflow?.edges],
|
|
@@ -1466,8 +1591,11 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1466
1591
|
}, [workflow?.id, workflowSnapshotKey, normalizeNodesForCanvas]);
|
|
1467
1592
|
|
|
1468
1593
|
useEffect(() => {
|
|
1469
|
-
if (!
|
|
1594
|
+
if (!workflow?.id) {
|
|
1470
1595
|
setLiveRun(null);
|
|
1596
|
+
setRecentRuns([]);
|
|
1597
|
+
setRecentRunsTotal(0);
|
|
1598
|
+
setRecentRunsLoading(false);
|
|
1471
1599
|
setLiveNodeStatuses({});
|
|
1472
1600
|
setLiveNodeOutputPreviews({});
|
|
1473
1601
|
setLiveNodeFlashStates({});
|
|
@@ -1479,9 +1607,13 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1479
1607
|
|
|
1480
1608
|
const pollLiveRun = async () => {
|
|
1481
1609
|
try {
|
|
1482
|
-
|
|
1610
|
+
setRecentRunsLoading(true);
|
|
1611
|
+
const data = await apiFetch(`/api/workflows/${encodeURIComponent(workflow.id)}/runs?limit=12`);
|
|
1483
1612
|
if (cancelled) return;
|
|
1484
1613
|
const runs = Array.isArray(data?.runs) ? data.runs : [];
|
|
1614
|
+
const total = Number(data?.pagination?.total);
|
|
1615
|
+
setRecentRuns(runs);
|
|
1616
|
+
setRecentRunsTotal(Number.isFinite(total) ? total : runs.length);
|
|
1485
1617
|
const running = runs.find((run) => run?.status === "running");
|
|
1486
1618
|
const targetRun = running || runs[0] || null;
|
|
1487
1619
|
if (!targetRun?.runId) {
|
|
@@ -1493,6 +1625,15 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1493
1625
|
setLiveEdgeActivity({});
|
|
1494
1626
|
return;
|
|
1495
1627
|
}
|
|
1628
|
+
if (!liveHighlightEnabled) {
|
|
1629
|
+
setLiveRun(targetRun);
|
|
1630
|
+
setLiveNodeStatuses({});
|
|
1631
|
+
setLiveNodeRunningHints({});
|
|
1632
|
+
setLiveNodeOutputPreviews({});
|
|
1633
|
+
setLiveNodeFlashStates({});
|
|
1634
|
+
setLiveEdgeActivity({});
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1496
1637
|
if (targetRun.status !== "running") {
|
|
1497
1638
|
setLiveRun(targetRun);
|
|
1498
1639
|
setLiveNodeStatuses({});
|
|
@@ -1533,6 +1674,8 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
1533
1674
|
});
|
|
1534
1675
|
} catch {
|
|
1535
1676
|
if (cancelled) return;
|
|
1677
|
+
} finally {
|
|
1678
|
+
if (!cancelled) setRecentRunsLoading(false);
|
|
1536
1679
|
}
|
|
1537
1680
|
};
|
|
1538
1681
|
|
|
@@ -2514,6 +2657,22 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2514
2657
|
<span class="btn-icon">${resolveIcon("play")}</span>
|
|
2515
2658
|
Run
|
|
2516
2659
|
<//>
|
|
2660
|
+
<${Button}
|
|
2661
|
+
variant="outlined"
|
|
2662
|
+
size="small"
|
|
2663
|
+
onClick=${() => openWorkflowRunsView(workflow?.id)}
|
|
2664
|
+
>
|
|
2665
|
+
<span class="btn-icon">${resolveIcon("chart")}</span>
|
|
2666
|
+
Runs
|
|
2667
|
+
<//>
|
|
2668
|
+
${workflow?.metadata?.installedFrom && html`<${Button}
|
|
2669
|
+
variant="outlined"
|
|
2670
|
+
size="small"
|
|
2671
|
+
onClick=${() => relayoutTemplateWorkflow(workflow.id)}
|
|
2672
|
+
>
|
|
2673
|
+
<span class="btn-icon">${resolveIcon("refresh")}</span>
|
|
2674
|
+
Re-layout
|
|
2675
|
+
<//>`}
|
|
2517
2676
|
${workflow?.core !== true && html`<${Button}
|
|
2518
2677
|
variant="outlined"
|
|
2519
2678
|
size="small"
|
|
@@ -2555,7 +2714,7 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2555
2714
|
</span>
|
|
2556
2715
|
`}
|
|
2557
2716
|
${liveHighlightEnabled && hasLiveStatuses && html`
|
|
2558
|
-
<span class="wf-badge" style="font-size: 11px; background:
|
|
2717
|
+
<span class="wf-badge" style="font-size: 11px; background: var(--accent-soft, rgba(59,130,246,0.18)); color: var(--accent, #60a5fa);">
|
|
2559
2718
|
${liveActiveNodes} active node${liveActiveNodes === 1 ? "" : "s"}
|
|
2560
2719
|
</span>
|
|
2561
2720
|
`}
|
|
@@ -2564,6 +2723,54 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2564
2723
|
<${Button} variant="text" size="small" onClick=${returnToWorkflowList}>← Back to Workflows<//>
|
|
2565
2724
|
</div>
|
|
2566
2725
|
|
|
2726
|
+
<div style="position: absolute; top: 64px; right: 12px; z-index: 18; width: min(340px, calc(100vw - 24px)); pointer-events: none;">
|
|
2727
|
+
<div style="pointer-events: auto; background: var(--bg-card, #2b2a27); border: 1px solid var(--color-border, #2a3040); border-radius: 12px; backdrop-filter: blur(8px); box-shadow: var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.28)); overflow: hidden; color: var(--color-text, #e8eaf0);">
|
|
2728
|
+
<div style="display:flex; align-items:center; gap:8px; padding:10px 12px; border-bottom: 1px solid var(--color-border, #2a3040);">
|
|
2729
|
+
<span class="icon-inline">${resolveIcon("chart")}</span>
|
|
2730
|
+
<div style="font-size: 12px; font-weight: 700; letter-spacing: 0.02em; flex:1;">Workflow Runs</div>
|
|
2731
|
+
<span class="wf-badge" style="font-size: 10px; background: var(--bg-secondary, #1f2937); color: var(--text-secondary, #cbd5e1);">${recentRunsTotal || recentRuns.length} total</span>
|
|
2732
|
+
${recentRuns.some((run) => run?.status === "running") && html`<span class="wf-badge" style="font-size: 10px; background: var(--accent-soft, rgba(59,130,246,0.18)); color: var(--accent, #60a5fa);">active</span>`}
|
|
2733
|
+
<${Button} variant="text" size="small" onClick=${() => setRunsPanelOpen((open) => !open)}>${runsPanelOpen ? "Hide" : "Show"}<//>
|
|
2734
|
+
</div>
|
|
2735
|
+
${runsPanelOpen && html`
|
|
2736
|
+
<div style="padding: 10px 10px 12px; display:flex; flex-direction:column; gap:8px;">
|
|
2737
|
+
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
2738
|
+
<${Button} variant="outlined" size="small" onClick=${() => openWorkflowRunsView(workflow?.id)}>View all runs<//>
|
|
2739
|
+
${liveRun?.runId && html`<${Button} variant="text" size="small" onClick=${() => openWorkflowRunsView(workflow?.id, liveRun.runId)}>Open current run<//>`}
|
|
2740
|
+
</div>
|
|
2741
|
+
${recentRunsLoading && recentRuns.length === 0 && html`<div style="font-size: 12px; color: var(--color-text-secondary, #8b95a5);">Loading recent runs…</div>`}
|
|
2742
|
+
${!recentRunsLoading && recentRuns.length === 0 && html`<div style="font-size: 12px; color: var(--color-text-secondary, #8b95a5);">No runs recorded for this workflow yet.</div>`}
|
|
2743
|
+
${recentRuns.map((run) => {
|
|
2744
|
+
const styles = getRunStatusBadgeStyles(run?.status);
|
|
2745
|
+
const lastActivityAt = getRunActivityAt(run);
|
|
2746
|
+
return html`
|
|
2747
|
+
<button
|
|
2748
|
+
key=${run.runId}
|
|
2749
|
+
type="button"
|
|
2750
|
+
onClick=${() => openWorkflowRunsView(workflow?.id, run.runId)}
|
|
2751
|
+
style="text-align:left; width:100%; border:1px solid ${run?.status === 'running' ? 'var(--accent, #60a5fa)' : 'var(--color-border, #2a3040)'}; border-radius:10px; background:var(--bg-secondary, #111827); color:inherit; padding:10px; display:flex; gap:10px; cursor:pointer; box-shadow: var(--shadow-sm, none);"
|
|
2752
|
+
>
|
|
2753
|
+
<div style="flex:1; min-width:0;">
|
|
2754
|
+
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
|
|
2755
|
+
<span class="wf-badge" style="background:${styles.bg}; color:${styles.color}; font-size:10px;">${run?.status || 'unknown'}</span>
|
|
2756
|
+
<span style="font-size:11px; color: var(--color-text-secondary, #94a3b8); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${String(run?.runId || '').slice(0, 12) || 'run'}</span>
|
|
2757
|
+
</div>
|
|
2758
|
+
<div style="font-size:12px; font-weight:600; color:var(--color-text, #e5e7eb); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${formatRelative(run?.startedAt)}</div>
|
|
2759
|
+
<div style="font-size:11px; color: var(--color-text-secondary, #8b95a5); margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
|
2760
|
+
${formatDuration(run?.status === 'running' && run?.startedAt ? Math.max(0, liveNowTick - Number(run.startedAt)) : Number(run?.duration) || 0)}
|
|
2761
|
+
${lastActivityAt ? ` · active ${formatRelative(lastActivityAt)}` : ''}
|
|
2762
|
+
${run?.errorCount ? ` · ${run.errorCount} error${run.errorCount === 1 ? '' : 's'}` : ''}
|
|
2763
|
+
</div>
|
|
2764
|
+
</div>
|
|
2765
|
+
<div style="display:flex; align-items:center; color:var(--color-text-secondary, #94a3b8);">${resolveIcon('arrow-right') || '→'}</div>
|
|
2766
|
+
</button>
|
|
2767
|
+
`;
|
|
2768
|
+
})}
|
|
2769
|
+
</div>
|
|
2770
|
+
`}
|
|
2771
|
+
</div>
|
|
2772
|
+
</div>
|
|
2773
|
+
|
|
2567
2774
|
<${NodePalette}
|
|
2568
2775
|
open=${showNodePalette}
|
|
2569
2776
|
nodeTypes=${availableNodeTypes}
|
|
@@ -2624,6 +2831,8 @@ function WorkflowCanvas({ workflow, onSave, nodeTypes: availableNodeTypes = [] }
|
|
|
2624
2831
|
const isSelected = selectedEdgeId.value === edge.id;
|
|
2625
2832
|
const hasCondition = !!edge.condition;
|
|
2626
2833
|
const edgeColor = sourcePort?.color || (hasCondition ? "#f59e0b" : "#6b7280");
|
|
2834
|
+
const edgePath = curvePath(from.x, from.y, to.x, to.y);
|
|
2835
|
+
const isActiveFlow = liveHighlightEnabled && liveEdgeActivity[edge.id];
|
|
2627
2836
|
return html`
|
|
2628
2837
|
<g key=${edge.id} class="wf-edge" onClick=${(e) => { e.stopPropagation(); selectedEdgeId.value = edge.id; }}>
|
|
2629
2838
|
<path
|
|
@@ -3931,6 +4140,8 @@ function resolveWorkflowTemplateSource(workflow, templateLookupById, templateLoo
|
|
|
3931
4140
|
function WorkflowListView() {
|
|
3932
4141
|
const wfs = workflows.value || [];
|
|
3933
4142
|
const tmpls = templates.value || [];
|
|
4143
|
+
const isWorkflowListLoading = workflowsLoading.value;
|
|
4144
|
+
const isTemplateListLoading = templatesLoading.value;
|
|
3934
4145
|
const installedTemplateIds = new Set();
|
|
3935
4146
|
wfs.forEach((wf) => {
|
|
3936
4147
|
if (wf.metadata?.installedFrom) installedTemplateIds.add(wf.metadata.installedFrom);
|
|
@@ -3998,10 +4209,22 @@ function WorkflowListView() {
|
|
|
3998
4209
|
<span class="btn-icon">${resolveIcon("chart")}</span>
|
|
3999
4210
|
Run History
|
|
4000
4211
|
<//>
|
|
4212
|
+
<${Button} type="button" variant="outlined" size="small" onClick=${() => relayoutInstalledTemplateWorkflows()}>
|
|
4213
|
+
<span class="btn-icon">${resolveIcon("refresh")}</span>
|
|
4214
|
+
Re-layout Installed Templates
|
|
4215
|
+
<//>
|
|
4001
4216
|
</div>
|
|
4002
4217
|
|
|
4003
4218
|
<!-- Active Workflows -->
|
|
4004
|
-
${
|
|
4219
|
+
${isWorkflowListLoading && html`
|
|
4220
|
+
<div style="text-align: center; padding: 40px 20px; background: var(--color-bg-secondary, #1a1f2e); border-radius: 12px; margin-bottom: 24px; border: 1px solid var(--color-border, #2a3040);">
|
|
4221
|
+
<${CircularProgress} size=${28} />
|
|
4222
|
+
<div style="font-size: 16px; font-weight: 600; margin-top: 12px; margin-bottom: 8px;">Loading workflows…</div>
|
|
4223
|
+
<div style="font-size: 13px; color: var(--color-text-secondary, #8b95a5);">Fetching installed workflows and template metadata.</div>
|
|
4224
|
+
</div>
|
|
4225
|
+
`}
|
|
4226
|
+
|
|
4227
|
+
${!isWorkflowListLoading && wfs.length > 0 && html`
|
|
4005
4228
|
<div style="margin-bottom: 24px;">
|
|
4006
4229
|
<h3 style="font-size: 14px; font-weight: 600; color: var(--color-text-secondary, #8b95a5); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px;">
|
|
4007
4230
|
Your Workflows (${wfs.length})
|
|
@@ -4147,7 +4370,7 @@ function WorkflowListView() {
|
|
|
4147
4370
|
</div>
|
|
4148
4371
|
`}
|
|
4149
4372
|
|
|
4150
|
-
${wfs.length === 0 && html`
|
|
4373
|
+
${!isWorkflowListLoading && wfs.length === 0 && html`
|
|
4151
4374
|
<div style="text-align: center; padding: 40px 20px; background: var(--color-bg-secondary, #1a1f2e); border-radius: 12px; margin-bottom: 24px; border: 1px solid var(--color-border, #2a3040);">
|
|
4152
4375
|
<div style="font-size: 36px; margin-bottom: 12px;">
|
|
4153
4376
|
<span class="icon-inline">${resolveIcon("refresh")}</span>
|
|
@@ -4175,15 +4398,21 @@ function WorkflowListView() {
|
|
|
4175
4398
|
<!-- Templates (grouped by category, deduped against installed) -->
|
|
4176
4399
|
<div>
|
|
4177
4400
|
<h3 style="font-size: 14px; font-weight: 600; color: var(--color-text-secondary, #8b95a5); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px;">
|
|
4178
|
-
Available Templates (${availableTemplates.length})${tmpls.length !== availableTemplates.length ? html` <span style="font-size: 11px; font-weight: 400; opacity: 0.6;">· ${tmpls.length - availableTemplates.length} installed</span>` : ""}
|
|
4401
|
+
Available Templates (${isTemplateListLoading ? "…" : availableTemplates.length})${!isTemplateListLoading && tmpls.length !== availableTemplates.length ? html` <span style="font-size: 11px; font-weight: 400; opacity: 0.6;">· ${tmpls.length - availableTemplates.length} installed</span>` : ""}
|
|
4179
4402
|
</h3>
|
|
4180
|
-
${
|
|
4403
|
+
${isTemplateListLoading && html`
|
|
4404
|
+
<div style="text-align: center; padding: 24px; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 8px;">
|
|
4405
|
+
<${CircularProgress} size=${18} />
|
|
4406
|
+
<span>Loading templates…</span>
|
|
4407
|
+
</div>
|
|
4408
|
+
`}
|
|
4409
|
+
${!isTemplateListLoading && availableTemplates.length === 0 && html`
|
|
4181
4410
|
<div style="text-align: center; padding: 24px; opacity: 0.5; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px;">
|
|
4182
4411
|
<span class="icon-inline">${resolveIcon("star")}</span>
|
|
4183
4412
|
<span>All templates are installed!</span>
|
|
4184
4413
|
</div>
|
|
4185
4414
|
`}
|
|
4186
|
-
${availableTemplateGroups.map((group) => html`
|
|
4415
|
+
${!isTemplateListLoading && availableTemplateGroups.map((group) => html`
|
|
4187
4416
|
<div key=${group.key} style="margin-bottom: 20px;">
|
|
4188
4417
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--color-border, #2a304060);">
|
|
4189
4418
|
<span class="icon-inline" style="font-size: 16px;">${resolveIcon(group.icon) || ICONS.dot}</span>
|
|
@@ -4236,6 +4465,16 @@ function WorkflowListView() {
|
|
|
4236
4465
|
* Run History View
|
|
4237
4466
|
* ═══════════════════════════════════════════════════════════════ */
|
|
4238
4467
|
|
|
4468
|
+
function normalizeLiveNodeStatus(status) {
|
|
4469
|
+
const s = String(status || "").trim().toLowerCase();
|
|
4470
|
+
if (!s) return "";
|
|
4471
|
+
if (s === "success" || s === "done" || s === "complete") return "completed";
|
|
4472
|
+
if (s === "fail" || s === "error" || s === "errored") return "failed";
|
|
4473
|
+
if (s === "in_progress" || s === "active" || s === "executing") return "running";
|
|
4474
|
+
if (s === "idle" || s === "queued") return "pending";
|
|
4475
|
+
return s;
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4239
4478
|
function getRunStatusBadgeStyles(status) {
|
|
4240
4479
|
const normalized = normalizeLiveNodeStatus(status) || String(status || "").trim().toLowerCase();
|
|
4241
4480
|
if (normalized === "completed" || normalized === "success") return { bg: "#10b98130", color: "#10b981" };
|
|
@@ -4245,6 +4484,31 @@ function getRunStatusBadgeStyles(status) {
|
|
|
4245
4484
|
return { bg: "#6b728030", color: "#9ca3af" };
|
|
4246
4485
|
}
|
|
4247
4486
|
|
|
4487
|
+
function getCanvasNodeExecutionVisuals(status, isSelected, selectedColor, flashState = "") {
|
|
4488
|
+
const normalized = normalizeLiveNodeStatus(status) || String(status || "").trim().toLowerCase();
|
|
4489
|
+
if (normalized === "running") {
|
|
4490
|
+
return { fill: "#10233f", stroke: "#60a5fa", strokeWidth: 2.5, filter: "url(#node-glow)" };
|
|
4491
|
+
}
|
|
4492
|
+
if (normalized === "failed" || normalized === "fail" || flashState === "fail") {
|
|
4493
|
+
return { fill: "#2a1217", stroke: "#ef4444", strokeWidth: 2.25, filter: "url(#node-shadow)" };
|
|
4494
|
+
}
|
|
4495
|
+
if (normalized === "completed" || normalized === "success" || flashState === "success") {
|
|
4496
|
+
return { fill: "#0f2a23", stroke: "#10b981", strokeWidth: 2, filter: "url(#node-shadow)" };
|
|
4497
|
+
}
|
|
4498
|
+
if (normalized === "skipped" || flashState === "skipped") {
|
|
4499
|
+
return { fill: "#1f2430", stroke: "#94a3b8", strokeWidth: 2, filter: "url(#node-shadow)" };
|
|
4500
|
+
}
|
|
4501
|
+
if (normalized === "waiting" || normalized === "pending") {
|
|
4502
|
+
return { fill: "#2f2310", stroke: "#f59e0b", strokeWidth: 2, filter: "url(#node-shadow)" };
|
|
4503
|
+
}
|
|
4504
|
+
return {
|
|
4505
|
+
fill: isSelected ? "#1e293b" : "#1a1f2e",
|
|
4506
|
+
stroke: isSelected ? selectedColor : "#2a3040",
|
|
4507
|
+
strokeWidth: isSelected ? 2 : 1,
|
|
4508
|
+
filter: isSelected ? "url(#node-glow)" : "url(#node-shadow)",
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4248
4512
|
function getNodeStatusRank(status) {
|
|
4249
4513
|
const normalized = normalizeLiveNodeStatus(status) || status;
|
|
4250
4514
|
if (normalized === "running") return 0;
|
|
@@ -4335,6 +4599,8 @@ function RunHistoryView() {
|
|
|
4335
4599
|
const hasMoreRuns = workflowRunsHasMore.value === true;
|
|
4336
4600
|
const loadingMoreRuns = workflowRunsLoadingMore.value === true;
|
|
4337
4601
|
const selectedRun = selectedRunDetail.value;
|
|
4602
|
+
const scopedWorkflowId = String(workflowRunsScopeId.value || "").trim();
|
|
4603
|
+
const scopedWorkflowName = scopedWorkflowId ? getWorkflowNameById(scopedWorkflowId) : "";
|
|
4338
4604
|
const workflowNameMap = new Map((workflows.value || []).map((wf) => [wf.id, wf.name]));
|
|
4339
4605
|
const [nowTick, setNowTick] = useState(Date.now());
|
|
4340
4606
|
const hasRunningRuns = runs.some((run) => run?.status === "running");
|
|
@@ -4462,6 +4728,7 @@ function RunHistoryView() {
|
|
|
4462
4728
|
<${Button} variant="text" size="small" onClick=${() => { selectedRunId.value = null; selectedRunDetail.value = null; }}>
|
|
4463
4729
|
← Back to Run History
|
|
4464
4730
|
<//>
|
|
4731
|
+
${selectedRun.workflowId && html`<${Button} variant="text" size="small" onClick=${() => openWorkflowCanvas(selectedRun.workflowId)}>Open Workflow<//>`}
|
|
4465
4732
|
<h2 style="margin: 0; font-size: 18px; font-weight: 700;">Run Details</h2>
|
|
4466
4733
|
<${Button} variant="text" size="small" onClick=${() => loadRunDetail(selectedRun.runId)}>Refresh<//>
|
|
4467
4734
|
</div>
|
|
@@ -4544,7 +4811,8 @@ function RunHistoryView() {
|
|
|
4544
4811
|
<div style="padding: 0 4px;">
|
|
4545
4812
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;">
|
|
4546
4813
|
<${Button} variant="text" size="small" onClick=${returnToWorkflowList}>← Back to Workflows<//>
|
|
4547
|
-
|
|
4814
|
+
${scopedWorkflowId && html`<${Button} variant="text" size="small" onClick=${() => openWorkflowCanvas(scopedWorkflowId)}>Open Workflow<//>`}
|
|
4815
|
+
<h2 style="margin: 0; font-size: 18px; font-weight: 700;">Run History${scopedWorkflowName ? ` · ${scopedWorkflowName}` : ""}</h2>
|
|
4548
4816
|
<${Button}
|
|
4549
4817
|
variant="text"
|
|
4550
4818
|
size="small"
|
|
@@ -4650,8 +4918,8 @@ function RunHistoryView() {
|
|
|
4650
4918
|
? Math.max(0, nowTick - run.startedAt)
|
|
4651
4919
|
: run.duration;
|
|
4652
4920
|
const borderColor = run.isStuck
|
|
4653
|
-
? "#
|
|
4654
|
-
: (run.status === "running" ? "#
|
|
4921
|
+
? "var(--accent-warning, #f59e0b)"
|
|
4922
|
+
: (run.status === "running" ? "var(--accent, #60a5fa)" : "var(--color-border, #2a3040)");
|
|
4655
4923
|
const triggerLabel = getWorkflowRunTriggerLabel(run);
|
|
4656
4924
|
return html`
|
|
4657
4925
|
<${Button}
|
|
@@ -4742,14 +5010,15 @@ export function WorkflowsTab() {
|
|
|
4742
5010
|
const route = routeParams.value || {};
|
|
4743
5011
|
const workflowId = String(route.workflowId || "").trim();
|
|
4744
5012
|
const runId = String(route.runId || "").trim();
|
|
5013
|
+
const runsWorkflowId = String(route.runsWorkflowId || "").trim();
|
|
4745
5014
|
const wantsRuns = Boolean(route.runsView) || Boolean(runId);
|
|
4746
5015
|
|
|
4747
5016
|
if (wantsRuns) {
|
|
4748
|
-
resetWorkflowRunsState();
|
|
5017
|
+
resetWorkflowRunsState(runsWorkflowId || null);
|
|
4749
5018
|
viewMode.value = "runs";
|
|
4750
|
-
loadRuns(null, { reset: true });
|
|
5019
|
+
loadRuns(runsWorkflowId || null, { reset: true });
|
|
4751
5020
|
if (runId) {
|
|
4752
|
-
loadRunDetail(runId);
|
|
5021
|
+
loadRunDetail(runId, { workflowId: runsWorkflowId || null });
|
|
4753
5022
|
} else {
|
|
4754
5023
|
selectedRunId.value = null;
|
|
4755
5024
|
selectedRunDetail.value = null;
|
|
@@ -4794,12 +5063,13 @@ export function WorkflowsTab() {
|
|
|
4794
5063
|
}
|
|
4795
5064
|
if (mode === "runs") {
|
|
4796
5065
|
if (selectedRunId.value) {
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
);
|
|
5066
|
+
const route = { runsView: true, runId: selectedRunId.value };
|
|
5067
|
+
if (workflowRunsScopeId.value) route.runsWorkflowId = workflowRunsScopeId.value;
|
|
5068
|
+
setRouteParams(route, { replace: true, skipGuard: true });
|
|
4801
5069
|
} else {
|
|
4802
|
-
|
|
5070
|
+
const route = { runsView: true };
|
|
5071
|
+
if (workflowRunsScopeId.value) route.runsWorkflowId = workflowRunsScopeId.value;
|
|
5072
|
+
setRouteParams(route, { replace: true, skipGuard: true });
|
|
4803
5073
|
}
|
|
4804
5074
|
return;
|
|
4805
5075
|
}
|