bosun 0.41.2 → 0.41.4

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.
Files changed (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
@@ -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 style="font-size: 20px">${resolveIcon("play")}</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 style="font-size: 20px">${resolveIcon("download")}</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
- setRouteParams({ runsView: true, runId }, { replace: false, skipGuard: true });
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 (!liveHighlightEnabled || !workflow?.id) {
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
- const data = await apiFetch(`/api/workflows/runs?workflowId=${encodeURIComponent(workflow.id)}&limit=10`);
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: #3b82f630; color: #60a5fa;">
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
- ${wfs.length > 0 && html`
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
- ${availableTemplates.length === 0 && html`
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
- <h2 style="margin: 0; font-size: 18px; font-weight: 700;">Run History</h2>
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
- ? "#f59e0b80"
4654
- : (run.status === "running" ? "#3b82f680" : "var(--color-border, #2a3040)");
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
- setRouteParams(
4798
- { runsView: true, runId: selectedRunId.value },
4799
- { replace: true, skipGuard: true },
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
- setRouteParams({ runsView: true }, { replace: true, skipGuard: true });
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
  }