@parallelworks/ui 0.17.0 → 0.19.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.
Files changed (169) hide show
  1. package/README.md +86 -6
  2. package/dist/ai/adapter/memoryStore.d.ts +8 -0
  3. package/dist/ai/adapter/mock/index.d.ts +2 -0
  4. package/dist/ai/adapter/mock/index.js +81 -0
  5. package/dist/ai/adapter/mock/mockAdapter.d.ts +4 -0
  6. package/dist/ai/adapter/openai/createOpenAIStream.d.ts +9 -0
  7. package/dist/ai/adapter/openai/errors.d.ts +3 -0
  8. package/dist/ai/adapter/openai/index.d.ts +6 -0
  9. package/dist/ai/adapter/openai/index.js +334 -0
  10. package/dist/ai/adapter/openai/openaiAdapter.d.ts +9 -0
  11. package/dist/ai/adapter/openai/responses.d.ts +4 -0
  12. package/dist/ai/adapter/openai/sse.d.ts +24 -0
  13. package/dist/ai/adapter/openai/wire.d.ts +40 -0
  14. package/dist/ai/adapter/types.d.ts +131 -0
  15. package/dist/ai/agent/classifier.d.ts +2 -0
  16. package/dist/ai/agent/diff.d.ts +35 -0
  17. package/dist/ai/agent/diffHighlight.d.ts +6 -0
  18. package/dist/ai/agent/index.d.ts +6 -0
  19. package/dist/ai/agent/permissions.d.ts +11 -0
  20. package/dist/ai/agent/toolfmt.d.ts +18 -0
  21. package/dist/ai/agent/transcript.d.ts +38 -0
  22. package/dist/ai/ai.css +325 -0
  23. package/dist/ai/components/AllocationSelector.d.ts +4 -0
  24. package/dist/ai/components/AttachmentManager.d.ts +1 -0
  25. package/dist/ai/components/AttachmentUpload.d.ts +24 -0
  26. package/dist/ai/components/BlockedGroupBanner.d.ts +1 -0
  27. package/dist/ai/components/BranchNavigator.d.ts +8 -0
  28. package/dist/ai/components/ChatEmptyState.d.ts +3 -0
  29. package/dist/ai/components/ChatInput.d.ts +45 -0
  30. package/dist/ai/components/ChatLayout.d.ts +5 -0
  31. package/dist/ai/components/ChatMessage.d.ts +24 -0
  32. package/dist/ai/components/ChatMessageList.d.ts +35 -0
  33. package/dist/ai/components/ChatSidebar.d.ts +5 -0
  34. package/dist/ai/components/ChatThread.d.ts +3 -0
  35. package/dist/ai/components/ComposerChrome.d.ts +18 -0
  36. package/dist/ai/components/ComposerFrame.d.ts +16 -0
  37. package/dist/ai/components/DragOverlay.d.ts +5 -0
  38. package/dist/ai/components/ModelSelector.d.ts +6 -0
  39. package/dist/ai/components/ShareDialog.d.ts +11 -0
  40. package/dist/ai/components/SlashMenu.d.ts +28 -0
  41. package/dist/ai/components/agent/ActivityLine.d.ts +21 -0
  42. package/dist/ai/components/agent/AgentMessageParts.d.ts +54 -0
  43. package/dist/ai/components/agent/PermissionPicker.d.ts +11 -0
  44. package/dist/ai/components/attachmentKeys.d.ts +4 -0
  45. package/dist/ai/core/ChatProvider.d.ts +66 -0
  46. package/dist/ai/core/KeyboardShortcutsProvider.d.ts +5 -0
  47. package/dist/ai/core/chatReducer.d.ts +114 -0
  48. package/dist/ai/core/config.d.ts +47 -0
  49. package/dist/ai/core/events.d.ts +1 -0
  50. package/dist/ai/core/exportConversation.d.ts +4 -0
  51. package/dist/ai/core/greeting.d.ts +3 -0
  52. package/dist/ai/core/parts.d.ts +11 -0
  53. package/dist/ai/core/useDragDrop.d.ts +14 -0
  54. package/dist/ai/core/useKeyboardShortcuts.d.ts +12 -0
  55. package/dist/ai/core/useSSEStream.d.ts +34 -0
  56. package/dist/ai/index.d.ts +40 -0
  57. package/dist/ai/index.js +6645 -0
  58. package/dist/ai/strings.d.ts +167 -0
  59. package/dist/ai/types.d.ts +236 -0
  60. package/dist/ai/ui/Avatar.d.ts +8 -0
  61. package/dist/ai/ui/Dropdown.d.ts +31 -0
  62. package/dist/ai/ui/Markdown.d.ts +8 -0
  63. package/dist/ai/ui/Notice.d.ts +21 -0
  64. package/dist/ai/ui/buttons.d.ts +1 -0
  65. package/dist/ai/ui/codeBreaks.d.ts +16 -0
  66. package/dist/ai/ui/prose.d.ts +1 -0
  67. package/dist/ai/ui/sidebar.d.ts +81 -0
  68. package/dist/ai/utils.d.ts +4 -0
  69. package/dist/assets/{editor.worker-CuBRIwi-.js → editor.worker-BmHUIVG4.js} +194 -163
  70. package/dist/assets/{json.worker-8ytS02vz.js → json.worker-D8bJ92nw.js} +384 -338
  71. package/dist/assets/{yaml.worker-DJC-VUcv.js → yaml.worker-C4gyGAko.js} +15657 -13823
  72. package/dist/chunks/{CodePreviewEditor-UAtnpmOV.js → CodePreviewEditor-YqQBz1YL.js} +2 -2
  73. package/dist/chunks/ConfirmModal-aOR4O1QE.js +79 -0
  74. package/dist/chunks/{lib-CUTRDpa6.js → FieldWrapper-Cszwg-4B.js} +52 -55
  75. package/dist/chunks/{CreateModal-BbV5cTj7.js → Markdown-CM-FenNE.js} +25 -97
  76. package/dist/chunks/Monaco-DJ8MW-Dq.js +110 -0
  77. package/dist/chunks/{PageHeader-MxuOs9cH.js → PageHeader-0kG_iypP.js} +170 -105
  78. package/dist/chunks/{Provider-DHmZ3v9i.js → Provider-CDYurraE.js} +60 -24
  79. package/dist/chunks/{SwitchToggle-BRQoEwLZ.js → SwitchToggle-DShNdcao.js} +143 -140
  80. package/dist/chunks/{Table-C6JdFFiJ.js → Table-DU2MW8xz.js} +6 -4
  81. package/dist/chunks/{Tooltip-n9w7Tkt1.js → Tooltip-K5HJr4_p.js} +32 -5
  82. package/dist/chunks/duration-BOv8Phl8.js +17 -0
  83. package/dist/chunks/{editor-mNznodJN.js → editor-Dk1rbV75.js} +33 -19
  84. package/dist/chunks/focus-D_iZJTW1.js +7 -0
  85. package/dist/chunks/{list-7k0eaI5N.js → list-CJm875m7.js} +497 -379
  86. package/dist/chunks/logviewer-BTKuQWzw.js +828 -0
  87. package/dist/chunks/memoryStore-CC91S5LP.js +152 -0
  88. package/dist/chunks/types-Yy0NrerF.js +11 -0
  89. package/dist/components/AddMenu.d.ts +1 -0
  90. package/dist/components/Avatar.d.ts +13 -0
  91. package/dist/components/Breadcrumbs.d.ts +1 -1
  92. package/dist/components/Dropdown.d.ts +3 -1
  93. package/dist/components/EmptyState.d.ts +1 -1
  94. package/dist/components/Indicator.d.ts +2 -2
  95. package/dist/components/Input.d.ts +1 -1
  96. package/dist/components/LetterBadge.d.ts +1 -1
  97. package/dist/components/MarkdownImpl.d.ts +1 -1
  98. package/dist/components/Provider.d.ts +18 -65
  99. package/dist/components/SettingRow.d.ts +1 -1
  100. package/dist/components/SettingsGroup.d.ts +1 -1
  101. package/dist/components/focus.d.ts +2 -0
  102. package/dist/components/keys.d.ts +12 -0
  103. package/dist/duration.d.ts +3 -0
  104. package/dist/editor/Monaco.d.ts +1 -20
  105. package/dist/editor/index.d.ts +2 -1
  106. package/dist/editor/index.js +2 -2
  107. package/dist/editor/yaml.d.ts +5 -0
  108. package/dist/engine.d.ts +98 -0
  109. package/dist/file-explorer/FileExplorer.d.ts +11 -2
  110. package/dist/file-explorer/FileExplorerContext.d.ts +1 -1
  111. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +1 -1
  112. package/dist/file-explorer/FilePreview/previewType.d.ts +1 -1
  113. package/dist/file-explorer/FileRow.d.ts +1 -1
  114. package/dist/file-explorer/helpers.d.ts +4 -4
  115. package/dist/file-explorer/index.d.ts +3 -3
  116. package/dist/file-explorer/index.js +632 -665
  117. package/dist/file-explorer/lib/types.d.ts +6 -1
  118. package/dist/file-explorer/lib/utils.d.ts +1 -3
  119. package/dist/file-explorer/useNearEnd.d.ts +1 -1
  120. package/dist/form/FieldWrapper.d.ts +2 -1
  121. package/dist/form/Fields/ResourceOptionIcon.d.ts +1 -1
  122. package/dist/form/Form.d.ts +7 -22
  123. package/dist/form/index.d.ts +11 -12
  124. package/dist/form/index.js +1333 -1454
  125. package/dist/form/lib.d.ts +6 -1
  126. package/dist/form/types/fieldComponentTypes.d.ts +4 -2
  127. package/dist/form/types/fieldTypes.d.ts +6 -20
  128. package/dist/form/useParsedOpts.d.ts +2 -2
  129. package/dist/form/utils/{slurmDuration.d.ts → duration.d.ts} +2 -2
  130. package/dist/{workflow → graph}/MatrixGroup.d.ts +2 -2
  131. package/dist/{workflow → graph}/index.d.ts +1 -1
  132. package/dist/{workflow → graph}/index.js +220 -185
  133. package/dist/{workflow → graph}/types.d.ts +3 -3
  134. package/dist/icons.d.ts +20 -21
  135. package/dist/icons.js +3 -3
  136. package/dist/index.d.ts +38 -39
  137. package/dist/index.js +355 -354
  138. package/dist/list/ListTable.d.ts +1 -1
  139. package/dist/list/ListViewControls.d.ts +2 -2
  140. package/dist/list/RowContextMenu.d.ts +9 -9
  141. package/dist/list/UserHoverCard.d.ts +45 -0
  142. package/dist/list/index.d.ts +9 -8
  143. package/dist/list/index.js +3 -3
  144. package/dist/logviewer/LogViewer.d.ts +1 -1
  145. package/dist/logviewer/index.d.ts +1 -0
  146. package/dist/logviewer/index.js +2 -742
  147. package/dist/logviewer/segments.d.ts +9 -0
  148. package/dist/styles/Geist-Variable.woff2 +0 -0
  149. package/dist/styles/GeistMono-Variable.woff2 +0 -0
  150. package/dist/styles/base.css +57 -0
  151. package/dist/styles/fonts.css +15 -0
  152. package/dist/styles/theme.css +9 -41
  153. package/dist/styles.css +1 -1
  154. package/dist/theme/index.js +2 -2
  155. package/package.json +45 -20
  156. package/LICENSE +0 -21
  157. package/dist/chunks/Monaco-DVyIpyv4.js +0 -1046
  158. package/dist/editor/usesGitlab.d.ts +0 -97
  159. package/dist/form/Fields/BucketReadOnlyField.d.ts +0 -7
  160. package/dist/form/types/workflow-schema.d.ts +0 -1145
  161. package/dist/form/utils/pwResolver.d.ts +0 -18
  162. package/dist/chunks/{BareModal-BqroLbZ5.js → BareModal-BtRiQezg.js} +1 -1
  163. package/dist/chunks/{MarkdownImpl-Bq4QhrtA.js → MarkdownImpl-CyeTVvSt.js} +1 -1
  164. /package/dist/{workflow → graph}/AnnotationBanner.d.ts +0 -0
  165. /package/dist/{workflow → graph}/Collapse.d.ts +0 -0
  166. /package/dist/{workflow → graph}/DependencyGraph.d.ts +0 -0
  167. /package/dist/{workflow → graph}/JobSummary.d.ts +0 -0
  168. /package/dist/{workflow → graph}/Reveal.d.ts +0 -0
  169. /package/dist/{workflow → graph}/TreeView.d.ts +0 -0
@@ -1,19 +1,19 @@
1
+ import { c as useRunFile, d as useWorkflowEngine, l as useSlots, r as useNavigation, u as useStrings } from "../chunks/Provider-CDYurraE.js";
1
2
  import { ArrowLeftIcon, ArrowRightIcon, ChevronDownIcon, ChevronRightIcon, DocumentIcon, ErrorIcon, InfoIcon, LoaderIcon, OpenInNewGraphIcon, WarningIcon } from "../icons.js";
2
- import { c as useRunFile, l as useSlots, r as useNavigation, u as useStrings } from "../chunks/Provider-DHmZ3v9i.js";
3
- import { n as TOOLTIP_ID, r as TooltipInfo } from "../chunks/Tooltip-n9w7Tkt1.js";
4
- import { t as Loader } from "../chunks/Loader-BXoXTZGc.js";
5
- import { n as modalPanelClasses, t as BareModal } from "../chunks/BareModal-BqroLbZ5.js";
3
+ import { n as TOOLTIP_ID, o as keyedByContent, r as TooltipInfo } from "../chunks/Tooltip-K5HJr4_p.js";
4
+ import { n as modalPanelClasses, t as BareModal } from "../chunks/BareModal-BtRiQezg.js";
6
5
  import { t as Indicator } from "../chunks/Indicator-hBSIxOPK.js";
7
- import { LogViewer } from "../logviewer/index.js";
6
+ import { t as Loader } from "../chunks/Loader-BXoXTZGc.js";
7
+ import { t as LogViewer } from "../chunks/logviewer-BTKuQWzw.js";
8
+ import { n as toAbsHumanDuration } from "../chunks/duration-BOv8Phl8.js";
8
9
  import cx from "classnames";
9
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
11
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
12
  import { flushSync } from "react-dom";
12
13
  import { DateTime } from "luxon";
13
- import { buildSubworkflowLogPath, collapseMatrixGroupsInLayout, collectExpansionKeys, computeMatrixAggStatus, detectMatrixGroups, dumpYaml, getAllDeps, getStepLabel, isRunActive, toAbsHumanDuration } from "@parallelworks/workflow-parser";
14
14
  import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
15
15
  import { AnimatePresence, motion } from "framer-motion";
16
- //#region src/workflow/AnnotationBanner.tsx
16
+ //#region src/graph/AnnotationBanner.tsx
17
17
  var typeOrder = {
18
18
  error: 0,
19
19
  warning: 1,
@@ -113,7 +113,7 @@ function AnnotationBanner({ executedJobs }) {
113
113
  ]
114
114
  }), expanded && /* @__PURE__ */ jsx("div", {
115
115
  className: "divide-y divide-[var(--theme-border)] max-h-[400px] overflow-y-auto",
116
- children: annotations.map((annotation, idx) => {
116
+ children: keyedByContent(annotations, (a) => a.id ?? `${a.type}:${a.title}:${a.message}`).map(({ key, item: annotation }) => {
117
117
  const config = typeConfig[annotation.type] || typeConfig.notice;
118
118
  const Icon = config.icon;
119
119
  return /* @__PURE__ */ jsxs("div", {
@@ -142,13 +142,13 @@ function AnnotationBanner({ executedJobs }) {
142
142
  })
143
143
  ]
144
144
  })]
145
- }, idx);
145
+ }, key);
146
146
  })
147
147
  })]
148
148
  });
149
149
  }
150
150
  //#endregion
151
- //#region src/workflow/Collapse.tsx
151
+ //#region src/graph/Collapse.tsx
152
152
  function Collapse({ expanded, children, releaseOverflow = true }) {
153
153
  const [overflowVisible, setOverflowVisible] = useState(false);
154
154
  return /* @__PURE__ */ jsx(AnimatePresence, {
@@ -185,7 +185,7 @@ function Collapse({ expanded, children, releaseOverflow = true }) {
185
185
  });
186
186
  }
187
187
  //#endregion
188
- //#region src/workflow/Reveal.tsx
188
+ //#region src/graph/Reveal.tsx
189
189
  var EASE = "cubic-bezier(0.25, 0.1, 0.25, 1)";
190
190
  var ROWS_TRANSITION = `grid-template-rows 200ms ${EASE}`;
191
191
  var FULL_TRANSITION = `${ROWS_TRANSITION}, width 200ms ${EASE}`;
@@ -239,20 +239,13 @@ function Reveal({ open, children, growWidth = false, onSettled }) {
239
239
  });
240
240
  }
241
241
  //#endregion
242
- //#region src/workflow/JobSummary.tsx
242
+ //#region src/graph/JobSummary.tsx
243
243
  function UsesIcon({ uses, subworkflow }) {
244
244
  const navigation = useNavigation();
245
245
  const slots = useSlots();
246
246
  const { jobActions } = useStrings();
247
247
  if (!uses || !slots.workflowIcon) return null;
248
- const open = () => {
249
- if (uses.startsWith("workflow/")) navigation.toWorkflow(uses.split("/")[1] ?? "");
250
- else if (subworkflow.sourceUrl) navigation.openExternal(subworkflow.sourceUrl);
251
- else if (uses.startsWith("github/")) {
252
- const [ownerRepo, ref] = uses.replace("github/", "").split("@");
253
- navigation.openExternal("https://github.com/" + ownerRepo + "/tree/" + ref);
254
- } else navigation.toMarketItem(uses.split("/")[1] ?? "");
255
- };
248
+ const open = () => navigation.openStepSource(uses, subworkflow.sourceUrl);
256
249
  return /* @__PURE__ */ jsx("button", {
257
250
  type: "button",
258
251
  "aria-label": jobActions.openOriginalWorkflow,
@@ -269,6 +262,7 @@ function UsesIcon({ uses, subworkflow }) {
269
262
  }
270
263
  function Joblist(inputs) {
271
264
  const { jobActions } = useStrings();
265
+ const engine = useWorkflowEngine();
272
266
  return inputs.jobNames.map((jobName) => {
273
267
  if (inputs.jobs[jobName]?._matrixGroup) return null;
274
268
  const jobStatus = inputs.jobs[jobName]?.status;
@@ -291,7 +285,8 @@ function Joblist(inputs) {
291
285
  children: /* @__PURE__ */ jsx("div", {
292
286
  className: "flex flex-col gap-y-0.5 mt-0.5",
293
287
  children: (inputs.jobs[jobName]?.steps || []).map((step, i) => {
294
- const stepLabel = getStepLabel(step, i);
288
+ const stepLabel = engine.stepLabel(step, i);
289
+ const stepKey = `${jobName}:${i}`;
295
290
  const subJobs = step.subworkflow?.jobs;
296
291
  const canExpand = !!inputs.renderSubworkflow && !!subJobs && Object.keys(subJobs).length > 0;
297
292
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -302,7 +297,7 @@ function Joblist(inputs) {
302
297
  className: cx(inputs.preview ? "ml-1.5" : "ml-4", "gap-x-1.5 flex items-center text-left"),
303
298
  "data-testid": stepLabel,
304
299
  children: [inputs.preview ? "↳" : /* @__PURE__ */ jsx(Indicator, { status: step.status }), /* @__PURE__ */ jsx("div", { children: stepLabel })]
305
- }), step.subworkflow && /* @__PURE__ */ jsxs(Fragment, { children: [
300
+ }), step.subworkflow && /* @__PURE__ */ jsxs(Fragment$1, { children: [
306
301
  inputs.onStepLogClick && /* @__PURE__ */ jsx("button", {
307
302
  type: "button",
308
303
  "aria-label": jobActions.openSubworkflowLogs,
@@ -332,7 +327,7 @@ function Joblist(inputs) {
332
327
  children: /* @__PURE__ */ jsx(OpenInNewGraphIcon, { className: "w-[1.2em] h-[1.2em]" })
333
328
  })
334
329
  ] })]
335
- }), canExpand && inputs.renderSubworkflow?.(jobName, i)] }, "step-" + jobName + "-" + i);
330
+ }), canExpand && inputs.renderSubworkflow?.(jobName, i)] }, stepKey);
336
331
  })
337
332
  })
338
333
  })]
@@ -340,14 +335,119 @@ function Joblist(inputs) {
340
335
  });
341
336
  }
342
337
  //#endregion
343
- //#region src/workflow/TreeView.tsx
338
+ //#region src/graph/MatrixGroup.tsx
339
+ /** Matrix group item in the summary sidebar. */
340
+ function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, jobs, isStepsOpen, onJobClick, onStepClick, onStepLogClick, onSubworkflowIcon, isSubworkflowExpanded, renderSubworkflow, preview }) {
341
+ return /* @__PURE__ */ jsxs("div", {
342
+ className: "py-1.5",
343
+ children: [/* @__PURE__ */ jsxs("button", {
344
+ type: "button",
345
+ "aria-expanded": isExpanded,
346
+ onClick: onToggle,
347
+ className: "cursor-pointer flex items-center gap-x-1.5 w-full text-left",
348
+ children: [/* @__PURE__ */ jsx(Indicator, { status }), /* @__PURE__ */ jsxs("div", {
349
+ className: "font-semibold",
350
+ children: [
351
+ "Matrix: ",
352
+ matrixGroup.originaljob,
353
+ /* @__PURE__ */ jsxs("span", {
354
+ className: "font-normal text-sm ml-1",
355
+ children: [
356
+ "(",
357
+ matrixGroup.members.length,
358
+ " jobs)"
359
+ ]
360
+ })
361
+ ]
362
+ })]
363
+ }), /* @__PURE__ */ jsx(Reveal, {
364
+ open: isExpanded,
365
+ children: /* @__PURE__ */ jsx("div", {
366
+ className: "ml-3 mt-0.5",
367
+ children: /* @__PURE__ */ jsx(Joblist, {
368
+ jobs,
369
+ jobNames: matrixGroup.members,
370
+ onJobClick,
371
+ isStepsOpen,
372
+ onStepClick,
373
+ onStepLogClick,
374
+ onSubworkflowIcon,
375
+ isSubworkflowExpanded,
376
+ renderSubworkflow,
377
+ preview
378
+ })
379
+ })
380
+ })]
381
+ }, "matrix-" + matrixGroup.originaljob);
382
+ }
383
+ /** Matrix group node in the DAG visualization. */
384
+ function MatrixGroupNode({ matrixGroup, jobNames, idPrefix, isExpanded, onToggle, jobs, activeDists, onMouseEnter, onMouseLeave, hoveredRelated, animT, zBase, isStepsOpen, onJobClick, onStepClick, onStepLogClick, onSubworkflowIcon, isSubworkflowExpanded, renderSubworkflow, preview }) {
385
+ const matrixName = matrixGroup.originaljob;
386
+ const matrixMembers = matrixGroup.members;
387
+ const { aggStatus, statusLabel } = useWorkflowEngine().matrixStatus(matrixMembers.map((m) => jobs[m]?.status));
388
+ return /* @__PURE__ */ jsxs("div", {
389
+ id: "node_" + idPrefix + jobNames[0],
390
+ role: "none",
391
+ className: "relative m-24",
392
+ style: { zIndex: (activeDists?.has(jobNames[0]) ? 25 : 1) + zBase },
393
+ onMouseEnter,
394
+ onMouseLeave,
395
+ onFocus: onMouseEnter,
396
+ onBlur: onMouseLeave,
397
+ children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-2xl bg-(--theme-panel-bg)" }), /* @__PURE__ */ jsxs("div", {
398
+ className: "relative border-solid shadow py-4 px-8 rounded-xl border-4 whitespace-nowrap bg-(--theme-panel-bg) text-2xl",
399
+ style: {
400
+ opacity: hoveredRelated && !hoveredRelated.has(jobNames[0]) ? .5 : 1,
401
+ transition: `opacity ${animT}s`
402
+ },
403
+ children: [/* @__PURE__ */ jsxs("button", {
404
+ type: "button",
405
+ "aria-expanded": isExpanded,
406
+ className: "cursor-pointer w-full text-left",
407
+ onClick: onToggle,
408
+ children: [/* @__PURE__ */ jsxs("div", {
409
+ className: "font-semibold text-md p-0.5",
410
+ children: ["Matrix: ", matrixName.length > 20 ? matrixName.slice(0, 20) + "..." : matrixName]
411
+ }), !isExpanded && /* @__PURE__ */ jsxs("div", {
412
+ className: "mt-1 border-t border-(--theme-border) pt-1 flex items-center gap-x-1",
413
+ children: [/* @__PURE__ */ jsx(Indicator, { status: aggStatus }), /* @__PURE__ */ jsx("span", { children: statusLabel })]
414
+ })]
415
+ }), /* @__PURE__ */ jsx(Reveal, {
416
+ open: isExpanded,
417
+ growWidth: true,
418
+ children: /* @__PURE__ */ jsx("div", {
419
+ className: "mt-1 border-t border-(--theme-border) pt-1",
420
+ children: /* @__PURE__ */ jsx(Joblist, {
421
+ jobs,
422
+ jobNames: matrixMembers,
423
+ onJobClick,
424
+ isStepsOpen,
425
+ onStepClick,
426
+ onStepLogClick,
427
+ onSubworkflowIcon,
428
+ isSubworkflowExpanded,
429
+ renderSubworkflow,
430
+ growWidth: true,
431
+ preview
432
+ })
433
+ })
434
+ })]
435
+ })]
436
+ }, "node_" + jobNames[0]);
437
+ }
438
+ //#endregion
439
+ //#region src/graph/TreeView.tsx
344
440
  function getStepDuration(step) {
345
441
  if (!step.startedAt) return "";
346
- return toAbsHumanDuration(DateTime.fromISO(step.startedAt), step.completedAt ? DateTime.fromISO(step.completedAt) : DateTime.now());
442
+ const start = DateTime.fromISO(step.startedAt);
443
+ const end = step.completedAt ? DateTime.fromISO(step.completedAt) : DateTime.now();
444
+ return toAbsHumanDuration(start, end);
347
445
  }
348
446
  function getJobDuration(job) {
349
447
  if (!job.startedAt) return "";
350
- return toAbsHumanDuration(DateTime.fromISO(job.startedAt), job.completedAt ? DateTime.fromISO(job.completedAt) : DateTime.now());
448
+ const start = DateTime.fromISO(job.startedAt);
449
+ const end = job.completedAt ? DateTime.fromISO(job.completedAt) : DateTime.now();
450
+ return toAbsHumanDuration(start, end);
351
451
  }
352
452
  function formatJobLabel(jobName, job) {
353
453
  const matrix = job?._matrix;
@@ -355,7 +455,8 @@ function formatJobLabel(jobName, job) {
355
455
  return (jobName[0].toUpperCase() + jobName.slice(1)).replace(/_/g, " ");
356
456
  }
357
457
  function InlineLogViewer({ slug, jobName, stepIndex }) {
358
- const { data: logs, isLoading, error } = useRunFile(slug, buildSubworkflowLogPath("", jobName, stepIndex), { refreshInterval: 2e3 });
458
+ const logPath = useWorkflowEngine().stepLogPath("", jobName, stepIndex);
459
+ const { data: logs, isLoading, error } = useRunFile(slug, logPath, { refreshInterval: 2e3 });
359
460
  if (isLoading && !logs) return /* @__PURE__ */ jsxs("div", {
360
461
  className: "flex items-center gap-2 px-5 py-4 text-sm text-[var(--theme-muted-text-color)]",
361
462
  children: [/* @__PURE__ */ jsx(LoaderIcon, { className: "w-4 h-4" }), /* @__PURE__ */ jsx("span", { children: "Loading logs..." })]
@@ -448,10 +549,11 @@ function JobRow({ jobName, job, isExpanded, onToggle, dependencies, workflowName
448
549
  className: "ml-[38px] border-l-2 border-[var(--theme-border)]",
449
550
  children: job.steps.map((step, index) => {
450
551
  const stepKey = `${jobName}:${index}`;
552
+ const isStepExpanded = expandedSteps.has(stepKey);
451
553
  return /* @__PURE__ */ jsx(StepRow, {
452
554
  step,
453
555
  index,
454
- isExpanded: expandedSteps.has(stepKey),
556
+ isExpanded: isStepExpanded,
455
557
  onToggle: () => onStepToggle(stepKey),
456
558
  workflowName,
457
559
  runNumber,
@@ -461,7 +563,7 @@ function JobRow({ jobName, job, isExpanded, onToggle, dependencies, workflowName
461
563
  onJobToggle,
462
564
  expandedSteps,
463
565
  onStepToggle
464
- }, index);
566
+ }, stepKey);
465
567
  })
466
568
  })
467
569
  })
@@ -469,9 +571,10 @@ function JobRow({ jobName, job, isExpanded, onToggle, dependencies, workflowName
469
571
  });
470
572
  }
471
573
  function StepRow({ step, index, isExpanded, onToggle, workflowName, runNumber, slug, jobName, expandedJobs, onJobToggle, expandedSteps, onStepToggle, depth = 0 }) {
574
+ const engine = useWorkflowEngine();
472
575
  const slots = useSlots();
473
576
  const duration = getStepDuration(step);
474
- const label = getStepLabel(step, index);
577
+ const label = engine.stepLabel(step, index);
475
578
  const isSubworkflow = !!step.subworkflow;
476
579
  const panelId = useId();
477
580
  const subworkflowJobs = step.subworkflow?.jobs ?? null;
@@ -583,10 +686,11 @@ function StepRow({ step, index, isExpanded, onToggle, workflowName, runNumber, s
583
686
  const subJob = subworkflowJobs?.[subJobName];
584
687
  if (!subJob) return null;
585
688
  const subJobKey = `${jobName}:${index}:${subJobName}`;
689
+ const isSubJobExpanded = expandedJobs.has(subJobKey);
586
690
  return /* @__PURE__ */ jsx(SubworkflowJobRow, {
587
691
  jobName: subJobName,
588
692
  job: subJob,
589
- isExpanded: expandedJobs.has(subJobKey),
693
+ isExpanded: isSubJobExpanded,
590
694
  onToggle: () => onJobToggle(subJobKey),
591
695
  dependencies: subworkflowDependenciesMap[subJobName],
592
696
  slug,
@@ -677,10 +781,11 @@ function SubworkflowJobRow({ jobName, job, isExpanded, onToggle, dependencies, s
677
781
  className: "ml-[38px] border-l-2 border-[var(--theme-border)]",
678
782
  children: job.steps.map((step, index) => {
679
783
  const stepKey = `${nestedSubworkflowPrefix}${jobName}:${index}`;
784
+ const isStepExpanded = expandedSteps.has(stepKey);
680
785
  return /* @__PURE__ */ jsx(SubworkflowStepRow, {
681
786
  step,
682
787
  index,
683
- isExpanded: expandedSteps.has(stepKey),
788
+ isExpanded: isStepExpanded,
684
789
  onToggle: () => onStepToggle(stepKey),
685
790
  slug,
686
791
  subworkflowPrefix: nestedSubworkflowPrefix,
@@ -690,7 +795,7 @@ function SubworkflowJobRow({ jobName, job, isExpanded, onToggle, dependencies, s
690
795
  expandedSteps,
691
796
  onStepToggle,
692
797
  depth
693
- }, index);
798
+ }, stepKey);
694
799
  })
695
800
  })
696
801
  })
@@ -698,9 +803,10 @@ function SubworkflowJobRow({ jobName, job, isExpanded, onToggle, dependencies, s
698
803
  });
699
804
  }
700
805
  function SubworkflowStepRow({ step, index, isExpanded, onToggle, slug, subworkflowPrefix, jobName, expandedJobs, onJobToggle, expandedSteps, onStepToggle, depth }) {
806
+ const engine = useWorkflowEngine();
701
807
  const slots = useSlots();
702
808
  const duration = getStepDuration(step);
703
- const label = getStepLabel(step, index);
809
+ const label = engine.stepLabel(step, index);
704
810
  const isSubworkflow = !!step.subworkflow;
705
811
  const panelId = useId();
706
812
  const subworkflowJobs = step.subworkflow?.jobs ?? null;
@@ -813,10 +919,11 @@ function SubworkflowStepRow({ step, index, isExpanded, onToggle, slug, subworkfl
813
919
  const subJob = subworkflowJobs?.[subJobName];
814
920
  if (!subJob) return null;
815
921
  const subJobKey = `${subworkflowPrefix}${jobName}:${index}:${subJobName}`;
922
+ const isSubJobExpanded = expandedJobs.has(subJobKey);
816
923
  return /* @__PURE__ */ jsx(SubworkflowJobRow, {
817
924
  jobName: subJobName,
818
925
  job: subJob,
819
- isExpanded: expandedJobs.has(subJobKey),
926
+ isExpanded: isSubJobExpanded,
820
927
  onToggle: () => onJobToggle(subJobKey),
821
928
  dependencies: subworkflowDependenciesMap[subJobName],
822
929
  slug,
@@ -836,7 +943,8 @@ function SubworkflowStepRow({ step, index, isExpanded, onToggle, slug, subworkfl
836
943
  ] });
837
944
  }
838
945
  function SubworkflowInlineLogViewer({ slug, subworkflowPrefix, jobName, stepIndex }) {
839
- const { data: logs, isLoading, error } = useRunFile(slug, buildSubworkflowLogPath(subworkflowPrefix, jobName, stepIndex), { refreshInterval: 2e3 });
946
+ const logPath = useWorkflowEngine().stepLogPath(subworkflowPrefix, jobName, stepIndex);
947
+ const { data: logs, isLoading, error } = useRunFile(slug, logPath, { refreshInterval: 2e3 });
840
948
  if (isLoading && !logs) return /* @__PURE__ */ jsxs("div", {
841
949
  className: "flex items-center gap-2 px-5 py-4 text-sm text-[var(--theme-muted-text-color)]",
842
950
  children: [/* @__PURE__ */ jsx(LoaderIcon, { className: "w-4 h-4" }), /* @__PURE__ */ jsx("span", { children: "Loading logs..." })]
@@ -858,6 +966,7 @@ function SubworkflowInlineLogViewer({ slug, subworkflowPrefix, jobName, stepInde
858
966
  });
859
967
  }
860
968
  function TreeView({ jobs, workflowName, runNumber, slug, expandedJobs, setExpandedJobs }) {
969
+ const engine = useWorkflowEngine();
861
970
  const visibleJobNames = useMemo(() => {
862
971
  return Object.keys(jobs).filter((jobName) => jobs[jobName]?.if !== false);
863
972
  }, [jobs]);
@@ -924,10 +1033,17 @@ function TreeView({ jobs, workflowName, runNumber, slug, expandedJobs, setExpand
924
1033
  const matrixGroups = {};
925
1034
  for (const jobName of visibleJobNames) {
926
1035
  const matrix = jobs[jobName]?._matrix;
927
- if (matrix?.originaljob) (matrixGroups[matrix.originaljob] ??= {
928
- originaljob: matrix.originaljob,
929
- members: []
930
- }).members.push(jobName);
1036
+ if (matrix?.originaljob) {
1037
+ let group = matrixGroups[matrix.originaljob];
1038
+ if (!group) {
1039
+ group = {
1040
+ originaljob: matrix.originaljob,
1041
+ members: []
1042
+ };
1043
+ matrixGroups[matrix.originaljob] = group;
1044
+ }
1045
+ group.members.push(jobName);
1046
+ }
931
1047
  }
932
1048
  for (const group of Object.values(matrixGroups)) group.members.sort((a, b) => (jobs[a]?._matrix?.index ?? 0) - (jobs[b]?._matrix?.index ?? 0));
933
1049
  const rendered = [];
@@ -940,7 +1056,7 @@ function TreeView({ jobs, workflowName, runNumber, slug, expandedJobs, setExpand
940
1056
  const group = matrixGroups[matrixOrigin];
941
1057
  if (!group) continue;
942
1058
  const isGroupExpanded = expandedJobs.has(`matrix:${matrixOrigin}`);
943
- const { aggStatus } = computeMatrixAggStatus(group.members.map((m) => jobs[m]?.status));
1059
+ const { aggStatus } = engine.matrixStatus(group.members.map((m) => jobs[m]?.status));
944
1060
  rendered.push(/* @__PURE__ */ jsxs("div", {
945
1061
  className: "border-b border-[var(--theme-border)]",
946
1062
  children: [/* @__PURE__ */ jsxs("button", {
@@ -1020,117 +1136,12 @@ function TreeView({ jobs, workflowName, runNumber, slug, expandedJobs, setExpand
1020
1136
  });
1021
1137
  }
1022
1138
  //#endregion
1023
- //#region src/workflow/MatrixGroup.tsx
1024
- /** Matrix group item in the summary sidebar. */
1025
- function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, jobs, isStepsOpen, onJobClick, onStepClick, onStepLogClick, onSubworkflowIcon, isSubworkflowExpanded, renderSubworkflow, preview }) {
1026
- return /* @__PURE__ */ jsxs("div", {
1027
- className: "py-1.5",
1028
- children: [/* @__PURE__ */ jsxs("button", {
1029
- type: "button",
1030
- "aria-expanded": isExpanded,
1031
- onClick: onToggle,
1032
- className: "cursor-pointer flex items-center gap-x-1.5 w-full text-left",
1033
- children: [/* @__PURE__ */ jsx(Indicator, { status }), /* @__PURE__ */ jsxs("div", {
1034
- className: "font-semibold",
1035
- children: [
1036
- "Matrix: ",
1037
- matrixGroup.originaljob,
1038
- /* @__PURE__ */ jsxs("span", {
1039
- className: "font-normal text-sm ml-1",
1040
- children: [
1041
- "(",
1042
- matrixGroup.members.length,
1043
- " jobs)"
1044
- ]
1045
- })
1046
- ]
1047
- })]
1048
- }), /* @__PURE__ */ jsx(Reveal, {
1049
- open: isExpanded,
1050
- children: /* @__PURE__ */ jsx("div", {
1051
- className: "ml-3 mt-0.5",
1052
- children: /* @__PURE__ */ jsx(Joblist, {
1053
- jobs,
1054
- jobNames: matrixGroup.members,
1055
- onJobClick,
1056
- isStepsOpen,
1057
- onStepClick,
1058
- onStepLogClick,
1059
- onSubworkflowIcon,
1060
- isSubworkflowExpanded,
1061
- renderSubworkflow,
1062
- preview
1063
- })
1064
- })
1065
- })]
1066
- }, "matrix-" + matrixGroup.originaljob);
1067
- }
1068
- /** Matrix group node in the DAG visualization. */
1069
- function MatrixGroupNode({ matrixGroup, jobNames, idPrefix, isExpanded, onToggle, jobs, activeDists, onMouseEnter, onMouseLeave, hoveredRelated, animT, zBase, isStepsOpen, onJobClick, onStepClick, onStepLogClick, onSubworkflowIcon, isSubworkflowExpanded, renderSubworkflow, preview }) {
1070
- const matrixName = matrixGroup.originaljob;
1071
- const matrixMembers = matrixGroup.members;
1072
- const { aggStatus, statusLabel } = computeMatrixAggStatus(matrixMembers.map((m) => jobs[m]?.status));
1073
- return /* @__PURE__ */ jsxs("div", {
1074
- id: "node_" + idPrefix + jobNames[0],
1075
- role: "none",
1076
- className: "relative m-24",
1077
- style: { zIndex: (activeDists?.has(jobNames[0]) ? 25 : 1) + zBase },
1078
- onMouseEnter,
1079
- onMouseLeave,
1080
- onFocus: onMouseEnter,
1081
- onBlur: onMouseLeave,
1082
- children: [/* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-2xl bg-(--theme-panel-bg)" }), /* @__PURE__ */ jsxs("div", {
1083
- className: "relative border-solid shadow py-4 px-8 rounded-xl border-4 whitespace-nowrap bg-(--theme-panel-bg) text-2xl",
1084
- style: {
1085
- opacity: hoveredRelated && !hoveredRelated.has(jobNames[0]) ? .5 : 1,
1086
- transition: `opacity ${animT}s`
1087
- },
1088
- children: [/* @__PURE__ */ jsxs("button", {
1089
- type: "button",
1090
- "aria-expanded": isExpanded,
1091
- className: "cursor-pointer w-full text-left",
1092
- onClick: onToggle,
1093
- children: [/* @__PURE__ */ jsxs("div", {
1094
- className: "font-semibold text-md p-0.5",
1095
- children: [
1096
- "Matrix:",
1097
- " ",
1098
- matrixName.length > 20 ? matrixName.slice(0, 20) + "..." : matrixName
1099
- ]
1100
- }), !isExpanded && /* @__PURE__ */ jsxs("div", {
1101
- className: "mt-1 border-t border-(--theme-border) pt-1 flex items-center gap-x-1",
1102
- children: [/* @__PURE__ */ jsx(Indicator, { status: aggStatus }), /* @__PURE__ */ jsx("span", { children: statusLabel })]
1103
- })]
1104
- }), /* @__PURE__ */ jsx(Reveal, {
1105
- open: isExpanded,
1106
- growWidth: true,
1107
- children: /* @__PURE__ */ jsx("div", {
1108
- className: "mt-1 border-t border-(--theme-border) pt-1",
1109
- children: /* @__PURE__ */ jsx(Joblist, {
1110
- jobs,
1111
- jobNames: matrixMembers,
1112
- onJobClick,
1113
- isStepsOpen,
1114
- onStepClick,
1115
- onStepLogClick,
1116
- onSubworkflowIcon,
1117
- isSubworkflowExpanded,
1118
- renderSubworkflow,
1119
- growWidth: true,
1120
- preview
1121
- })
1122
- })
1123
- })]
1124
- })]
1125
- }, "node_" + jobNames[0]);
1126
- }
1127
- //#endregion
1128
- //#region src/workflow/types.ts
1139
+ //#region src/graph/types.ts
1129
1140
  function isJobRecord(value) {
1130
1141
  return typeof value === "object" && value !== null;
1131
1142
  }
1132
1143
  //#endregion
1133
- //#region src/workflow/DependencyGraph.tsx
1144
+ //#region src/graph/DependencyGraph.tsx
1134
1145
  function ViewToggle({ viewMode, setViewMode }) {
1135
1146
  return /* @__PURE__ */ jsxs("div", {
1136
1147
  className: "flex border border-[var(--theme-border)] rounded overflow-hidden",
@@ -1235,12 +1246,12 @@ function parsePrefixToPath(prefix) {
1235
1246
  }
1236
1247
  return path;
1237
1248
  }
1238
- function computeGraphLayout(jobs, matrixGroups) {
1249
+ function computeGraphLayout(engine, jobs, matrixGroups) {
1239
1250
  const visibleJobs = structuredClone(jobs);
1240
1251
  for (const jobName of Object.keys(jobs ?? {})) if (jobs[jobName]?.if === false) delete visibleJobs[jobName];
1241
- collapseMatrixGroupsInLayout(matrixGroups, visibleJobs);
1252
+ engine.collapseMatrixGroups(matrixGroups, visibleJobs);
1242
1253
  const allDeps = {};
1243
- for (const jobName of Object.keys(visibleJobs)) allDeps[jobName] = getAllDeps(jobName, visibleJobs, allDeps);
1254
+ for (const jobName of Object.keys(visibleJobs)) allDeps[jobName] = engine.jobDependencies(jobName, visibleJobs, allDeps);
1244
1255
  const filteredDeps = {};
1245
1256
  for (const jobName of Object.keys(visibleJobs)) {
1246
1257
  const needs = visibleJobs[jobName]?.needs;
@@ -1253,19 +1264,19 @@ function computeGraphLayout(jobs, matrixGroups) {
1253
1264
  }).sort();
1254
1265
  }
1255
1266
  const depended_on = {};
1256
- Object.keys(visibleJobs).map((jobName) => {
1267
+ Object.keys(visibleJobs).forEach((jobName) => {
1257
1268
  if (filteredDeps[jobName] && Array.isArray(filteredDeps[jobName])) filteredDeps[jobName].forEach((dep) => {
1258
1269
  if (depended_on[dep]) depended_on[dep].push(jobName);
1259
1270
  else depended_on[dep] = [jobName];
1260
1271
  });
1261
1272
  });
1262
- Object.keys(visibleJobs).map((jobName) => {
1273
+ Object.keys(visibleJobs).forEach((jobName) => {
1263
1274
  if (depended_on[jobName]) depended_on[jobName].sort();
1264
1275
  });
1265
1276
  const unfinished = structuredClone(visibleJobs);
1266
1277
  const rootLevel = [];
1267
1278
  const deps = [rootLevel];
1268
- Object.keys(visibleJobs).map((jobName) => {
1279
+ Object.keys(visibleJobs).forEach((jobName) => {
1269
1280
  if (filteredDeps[jobName]?.length === 0) {
1270
1281
  rootLevel.push(jobName);
1271
1282
  delete unfinished[jobName];
@@ -1276,7 +1287,7 @@ function computeGraphLayout(jobs, matrixGroups) {
1276
1287
  const toAppend = [];
1277
1288
  const unfinishedArr = Object.keys(unfinished);
1278
1289
  prevLen = unfinishedArr.length;
1279
- unfinishedArr.map((jobName) => {
1290
+ unfinishedArr.forEach((jobName) => {
1280
1291
  if ((filteredDeps[jobName] ?? []).every((dep) => !unfinishedArr.includes(dep))) {
1281
1292
  toAppend.push(jobName);
1282
1293
  delete unfinished[jobName];
@@ -1285,14 +1296,14 @@ function computeGraphLayout(jobs, matrixGroups) {
1285
1296
  if (toAppend.length > 0) deps.push(toAppend);
1286
1297
  } while (prevLen !== Object.keys(unfinished).length);
1287
1298
  const finalDeps = [];
1288
- deps.map((col) => {
1299
+ deps.forEach((col) => {
1289
1300
  const toAppend = [];
1290
1301
  while (col.length > 0) {
1291
1302
  const head = col[0];
1292
1303
  if (head === void 0) break;
1293
1304
  const grouped = [head];
1294
1305
  const toRm = [0];
1295
- col.slice(1).map((jobName, j) => {
1306
+ col.slice(1).forEach((jobName, j) => {
1296
1307
  if (visibleJobs[head]?._matrixGroup || visibleJobs[jobName]?._matrixGroup) return;
1297
1308
  const depsA = depended_on[head];
1298
1309
  const depsB = depended_on[jobName];
@@ -1375,14 +1386,19 @@ function SubworkflowWell({ expanded, onSettled, className, children }) {
1375
1386
  var SUBGRAPH_Z_STEP = 100;
1376
1387
  var FIT_ANIMATION_MS = 300;
1377
1388
  function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflows, toggleSubworkflow, openInNewGraph, setSublogOpen, invalidateConnectors, requestFit, stepsOpen, toggleSteps, matrixOpen, toggleMatrix, onReady }) {
1389
+ const engine = useWorkflowEngine();
1378
1390
  const wrapperRef = useRef(null);
1379
1391
  const zBase = depth * SUBGRAPH_Z_STEP;
1380
1392
  const [hoveredJob, setHoveredJob] = useState(null);
1381
1393
  const lastHoveredDistancesRef = useRef(null);
1382
1394
  const maxHoveredDistRef = useRef(0);
1383
1395
  const retractPathLensRef = useRef(/* @__PURE__ */ new Map());
1384
- const matrixGroups = useMemo(() => detectMatrixGroups(jobs), [jobs]);
1385
- const { dependencyCols, directDeps, displayJobs, ancestorDists, descendantDists } = useMemo(() => computeGraphLayout(jobs, matrixGroups), [jobs, matrixGroups]);
1396
+ const matrixGroups = useMemo(() => engine.matrixGroups(jobs), [engine, jobs]);
1397
+ const { dependencyCols, directDeps, displayJobs, ancestorDists, descendantDists } = useMemo(() => computeGraphLayout(engine, jobs, matrixGroups), [
1398
+ engine,
1399
+ jobs,
1400
+ matrixGroups
1401
+ ]);
1386
1402
  const hoveredDistances = useMemo(() => {
1387
1403
  if (!hoveredJob) return null;
1388
1404
  const dist = /* @__PURE__ */ new Map();
@@ -1474,9 +1490,9 @@ function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflow
1474
1490
  const stepStatus = jobs[j]?.steps?.[+s]?.status;
1475
1491
  if (stepStatus === "skipped" || stepStatus === "skipped-failed") return;
1476
1492
  if (jobs[j]?.steps[+s]?.subworkflow) expandInline(j, +s);
1477
- else setSublogOpen(buildSubworkflowLogPath(pathPrefix, j, s));
1493
+ else setSublogOpen(engine.stepLogPath(pathPrefix, j, s));
1478
1494
  };
1479
- const onStepLogClick = (j, s) => setSublogOpen(buildSubworkflowLogPath(pathPrefix, j, s));
1495
+ const onStepLogClick = (j, s) => setSublogOpen(engine.stepLogPath(pathPrefix, j, s));
1480
1496
  const onSubworkflowIcon = (j, s) => openNew(j, s);
1481
1497
  const isSubworkflowExpanded = (j, s) => expandedSubworkflows.has(`${pathPrefix}${j}:${s}`);
1482
1498
  const renderSubworkflow = (j, s) => {
@@ -1486,7 +1502,7 @@ function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflow
1486
1502
  expanded: isSubworkflowExpanded(j, s),
1487
1503
  onSettled: requestFit,
1488
1504
  className: "my-2 rounded-lg border-4 border-(--theme-border)",
1489
- children: /* @__PURE__ */ jsx(Subgraph, {
1505
+ children: /* @__PURE__ */ jsx(NestedSubgraph, {
1490
1506
  jobs: subJobs,
1491
1507
  pathPrefix: `${pathPrefix}subworkflows/${j}/step_${s}/`,
1492
1508
  depth: depth + 1,
@@ -1584,6 +1600,7 @@ function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflow
1584
1600
  toggleMatrix,
1585
1601
  openInNewGraph,
1586
1602
  setSublogOpen,
1603
+ engine,
1587
1604
  invalidateConnectors,
1588
1605
  requestFit
1589
1606
  ]);
@@ -1772,6 +1789,7 @@ function Subgraph({ jobs, pathPrefix, depth, preview, animT, expandedSubworkflow
1772
1789
  })(), hasLiveNestedGraph ? buildBoxLayer() : memoizedBoxLayer]
1773
1790
  });
1774
1791
  }
1792
+ var NestedSubgraph = Subgraph;
1775
1793
  function SidebarWell({ expanded, children }) {
1776
1794
  return /* @__PURE__ */ jsx(Collapse, {
1777
1795
  expanded,
@@ -1780,8 +1798,13 @@ function SidebarWell({ expanded, children }) {
1780
1798
  });
1781
1799
  }
1782
1800
  function SidebarJobs({ jobs, pathPrefix, preview, stepsOpen, toggleSteps, expandedSubworkflows, toggleSubworkflow, matrixOpen, toggleMatrix, openInNewGraph, setSublogOpen }) {
1783
- const matrixGroups = useMemo(() => detectMatrixGroups(jobs), [jobs]);
1784
- const { displayJobs } = useMemo(() => computeGraphLayout(jobs, matrixGroups), [jobs, matrixGroups]);
1801
+ const engine = useWorkflowEngine();
1802
+ const matrixGroups = useMemo(() => engine.matrixGroups(jobs), [engine, jobs]);
1803
+ const { displayJobs } = useMemo(() => computeGraphLayout(engine, jobs, matrixGroups), [
1804
+ engine,
1805
+ jobs,
1806
+ matrixGroups
1807
+ ]);
1785
1808
  const isStepsOpen = (j) => stepsOpen.has(pathPrefix + j);
1786
1809
  const onJobClick = (j) => {
1787
1810
  const status = jobs[j]?.status ?? displayJobs[j]?.status;
@@ -1794,9 +1817,9 @@ function SidebarJobs({ jobs, pathPrefix, preview, stepsOpen, toggleSteps, expand
1794
1817
  const stepStatus = jobs[j]?.steps?.[+s]?.status;
1795
1818
  if (stepStatus === "skipped" || stepStatus === "skipped-failed") return;
1796
1819
  if (jobs[j]?.steps[+s]?.subworkflow) expandInline(j, +s);
1797
- else setSublogOpen(buildSubworkflowLogPath(pathPrefix, j, s));
1820
+ else setSublogOpen(engine.stepLogPath(pathPrefix, j, s));
1798
1821
  };
1799
- const onStepLogClick = (j, s) => setSublogOpen(buildSubworkflowLogPath(pathPrefix, j, s));
1822
+ const onStepLogClick = (j, s) => setSublogOpen(engine.stepLogPath(pathPrefix, j, s));
1800
1823
  const onSubworkflowIcon = (j, s) => openNew(j, s);
1801
1824
  const isSubworkflowExpanded = (j, s) => expandedSubworkflows.has(`${pathPrefix}${j}:${s}`);
1802
1825
  const renderSubworkflow = (j, s) => {
@@ -1823,13 +1846,14 @@ function SidebarJobs({ jobs, pathPrefix, preview, stepsOpen, toggleSteps, expand
1823
1846
  });
1824
1847
  };
1825
1848
  const seenMatrixGroups = /* @__PURE__ */ new Set();
1826
- return /* @__PURE__ */ jsx(Fragment, { children: Object.keys(jobs).flatMap((jobName) => {
1849
+ const orderedKeys = Object.keys(jobs).flatMap((jobName) => {
1827
1850
  const originaljob = jobs[jobName]?._matrix?.originaljob;
1828
1851
  if (!originaljob) return [jobName];
1829
1852
  if (seenMatrixGroups.has(originaljob)) return [];
1830
1853
  seenMatrixGroups.add(originaljob);
1831
1854
  return [originaljob];
1832
- }).map((jobName) => {
1855
+ });
1856
+ return /* @__PURE__ */ jsx(Fragment$1, { children: orderedKeys.map((jobName) => {
1833
1857
  const mg = displayJobs[jobName]?._matrixGroup;
1834
1858
  if (mg) return /* @__PURE__ */ jsx(MatrixGroupSummaryItem, {
1835
1859
  matrixGroup: mg,
@@ -1885,6 +1909,7 @@ function walkJobPath(root, path) {
1885
1909
  return isJobRecord(cursor) ? cursor : {};
1886
1910
  }
1887
1911
  function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder, viewMode = "dag", setViewMode }) {
1912
+ const engine = useWorkflowEngine();
1888
1913
  const animT = .125;
1889
1914
  const minScale = .2;
1890
1915
  const maxScale = .6;
@@ -2039,8 +2064,12 @@ function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder,
2039
2064
  const sublogIsStep = jobAndStep.length > 1;
2040
2065
  const logName = jobAndStep.length > 1 ? "Job: " + (jobAndStep[0][0].toUpperCase() + jobAndStep[0].slice(1)).replace(/_/g, " ") + " \nStep: " + (jobs[jobAndStep[0]]?.steps[Number(jobAndStep[1])]?.name || "Step " + jobAndStep[1]) : "";
2041
2066
  const sublogIsSubworkflow = jobAndStep.length > 1 && jobs[jobAndStep[0]]?.steps[Number(jobAndStep[1])]?.subworkflow;
2042
- const matrixGroups = useMemo(() => detectMatrixGroups(jobs), [jobs]);
2043
- const { dependencyCols } = useMemo(() => computeGraphLayout(jobs, matrixGroups), [jobs, matrixGroups]);
2067
+ const matrixGroups = useMemo(() => engine.matrixGroups(jobs), [engine, jobs]);
2068
+ const { dependencyCols } = useMemo(() => computeGraphLayout(engine, jobs, matrixGroups), [
2069
+ engine,
2070
+ jobs,
2071
+ matrixGroups
2072
+ ]);
2044
2073
  const fitGraph = useCallback((animationMs = 0) => {
2045
2074
  const container = containerRef.current;
2046
2075
  const content = graphContentRef.current;
@@ -2104,11 +2133,15 @@ function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder,
2104
2133
  for (let i = 0; i < nestingDepth - 1; i++) parentPrefix += `subworkflows/${currentPath[5 * i]}/step_${currentPath[5 * i + 2]?.toString()}/`;
2105
2134
  const lastJob = String(currentPath[5 * (nestingDepth - 1)] ?? "");
2106
2135
  const lastStep = currentPath[5 * (nestingDepth - 1) + 2] ?? "";
2107
- generalLogPath = buildSubworkflowLogPath(parentPrefix, lastJob, lastStep);
2136
+ generalLogPath = engine.stepLogPath(parentPrefix, lastJob, lastStep);
2108
2137
  }
2109
2138
  const missingLogText = sublogIsSubworkflow ? "Subworkflows generally have no logs/scripts at this level.\nClick on the \"Open in new graph\" icon on the subworkflow step to open the subworkflow graph." : sublogsError ? dagStrings.logLoadFailed : (sublogOpen === generalLogPath ? dagStrings.statusReason(run.statusReason) : void 0) ?? dagStrings.noLogFound;
2110
- const currentSublog = sublogOpen === "" ? "" : preview ? dumpYaml(jobs[jobAndStep[0]]?.steps[Number(jobAndStep[1])]) : sublogsError || !sublogs ? missingLogText : sublogs;
2111
- const expansionKeys = useMemo(() => collectExpansionKeys(jobs, pathAppend), [jobs, pathAppend]);
2139
+ const currentSublog = sublogOpen === "" ? "" : preview ? engine.toYaml(jobs[jobAndStep[0]]?.steps[Number(jobAndStep[1])]) : sublogsError || !sublogs ? missingLogText : sublogs;
2140
+ const expansionKeys = useMemo(() => engine.expansionKeys(jobs, pathAppend), [
2141
+ engine,
2142
+ jobs,
2143
+ pathAppend
2144
+ ]);
2112
2145
  const hasExpandable = expansionKeys.subworkflowKeys.length > 0 || expansionKeys.matrixKeys.length > 0;
2113
2146
  const allExpanded = hasExpandable && expansionKeys.subworkflowKeys.every((k) => expandedSubworkflows.has(k)) && expansionKeys.matrixKeys.every((k) => matrixOpen.has(k));
2114
2147
  const toggleExpandAll = () => {
@@ -2132,13 +2165,14 @@ function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder,
2132
2165
  let step = parseInt(sublogSplit[sublogSplit.length - 2].slice(5), 10);
2133
2166
  const jobArr = Object.keys(jobs);
2134
2167
  let idx = jobArr.indexOf(job);
2135
- if (backward) if (0 < step) step -= 1;
2136
- else {
2137
- if (idx === 0) idx = jobArr.length;
2138
- job = jobArr[idx - 1] ?? "";
2139
- step = (jobs[job]?.steps.length ?? 0) - 1;
2140
- }
2141
- else if ((jobs[job]?.steps.length ?? 0) - 1 > step) step += 1;
2168
+ if (backward) {
2169
+ if (0 < step) step -= 1;
2170
+ else {
2171
+ if (idx === 0) idx = jobArr.length;
2172
+ job = jobArr[idx - 1] ?? "";
2173
+ step = (jobs[job]?.steps.length ?? 0) - 1;
2174
+ }
2175
+ } else if ((jobs[job]?.steps.length ?? 0) - 1 > step) step += 1;
2142
2176
  else {
2143
2177
  if (idx === jobArr.length - 1) idx = -1;
2144
2178
  job = jobArr[idx + 1] ?? "";
@@ -2181,8 +2215,9 @@ function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder,
2181
2215
  ]);
2182
2216
  if (!Array.isArray(dependencyCols) || dependencyCols.length === 0 || !Array.isArray(dependencyCols[0]) || dependencyCols[0].length === 0 || !Array.isArray(dependencyCols[0][0]) || dependencyCols[0][0].length === 0) return;
2183
2217
  const startDt = run.createdAt ? DateTime.fromISO(run.createdAt) : now;
2184
- const runtime = toAbsHumanDuration(startDt, run.completedAt ? DateTime.fromISO(run.completedAt) : now);
2185
- const additionalBottomLeftBarComponents = sublogIsStep ? /* @__PURE__ */ jsxs(Fragment, { children: [
2218
+ const endDt = run.completedAt ? DateTime.fromISO(run.completedAt) : now;
2219
+ const runtime = toAbsHumanDuration(startDt, endDt);
2220
+ const additionalBottomLeftBarComponents = sublogIsStep ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
2186
2221
  /* @__PURE__ */ jsxs("button", {
2187
2222
  type: "button",
2188
2223
  className: "btn btn-neutral",
@@ -2222,7 +2257,7 @@ function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder,
2222
2257
  children: slots.runSessions({
2223
2258
  runId: run.id ?? "",
2224
2259
  runNumber,
2225
- runActive: isRunActive(run.status),
2260
+ runActive: engine.isRunActive(run.status),
2226
2261
  links: run.links
2227
2262
  })
2228
2263
  }),
@@ -2348,7 +2383,7 @@ function DependencyGraph({ run, preview = false, initialScale = 1, removeBorder,
2348
2383
  onTransform: invalidateConnectors,
2349
2384
  onWheel: invalidateConnectors,
2350
2385
  onZoom: invalidateConnectors,
2351
- children: () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
2386
+ children: () => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
2352
2387
  className: "absolute z-10 top-2 right-2 flex flex-col items-end gap-y-1",
2353
2388
  children: [
2354
2389
  hasExpandable && /* @__PURE__ */ jsx("button", {