@parallelworks/ui 0.74.0 → 0.75.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,7 +99,7 @@ Each need has one component. Reach for it before styling markup by hand.
99
99
  | A live relative time or duration | `RelativeTime`, `Elapsed` |
100
100
  | Rendered Markdown | `Markdown` |
101
101
  | Code or YAML to read or edit | `Editor` from `./editor`; `EditorField` as a form field, `FormCodePanel` for a form's values as JSON |
102
- | A run's jobs, steps, logs and annotations | `DependencyGraph` from `./graph`, as a graph or a list |
102
+ | A run's jobs, steps, logs, annotations and warnings | `DependencyGraph` from `./graph`, as a graph or a list |
103
103
  | A workflow's jobs drawn from its YAML, or edited by hand | `DependencyGraphPreview` from `./graph`, editable with an `editor`; `InputsFormEditor` for its run form |
104
104
  | A value that survives a reload | `usePersistedState` |
105
105
  | Choosing the app's colors | `ThemeEditor`, applied with `useApplyTheme` |
@@ -775,6 +775,14 @@ highlight changes, and panning, zooming and a running run's clock re-render none
775
775
  the whole graph, zooms out to 1% to show the shape of the largest, and in to 200% to read a node;
776
776
  the wheel zooms by the same factor at every scale, about the pointer.
777
777
 
778
+ A run's `warnings` (`RunWarning`: a `message` about a `job`, or about one of its steps by `step`
779
+ index, in the subworkflow its `path` of `[job, 'step_<n>', …]` names) are listed over the graph,
780
+ each linking to its job or step: the link opens the subworkflows, matrix and job around it, brings
781
+ it and what it is in into view below the graph's view bar, readable, and moves focus to it. Each job and step a warning is about, and each one it sits
782
+ inside, carries a mark that lists the warnings on hover, so one skipped without a log is still
783
+ flagged. A job name a matrix ran as several jobs marks each of them. `dag.warningCount` words the
784
+ count.
785
+
778
786
  The editor checks YAML against the schemas a host registers with `configureEditorYaml` from
779
787
  `@parallelworks/ui/editor`. The job, step, input and settings dialogs edit their YAML at their own
780
788
  paths (`JOB_YAML_PATH`, `STEP_YAML_PATH`, `INPUT_YAML_PATH`, `SETTINGS_YAML_PATH`); `settingsSchemas`
@@ -1,4 +1,4 @@
1
- import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, UnreachableSession } from '../types.js';
1
+ import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, ToolDecision, UnreachableSession } from '../types.js';
2
2
  import type { CompletionMessage } from './openai/wire.js';
3
3
  export declare class ChatAdapterError extends Error {
4
4
  status?: number | undefined;
@@ -48,6 +48,10 @@ export type PartDelta = {
48
48
  id: string;
49
49
  result: string;
50
50
  isError?: boolean;
51
+ } | {
52
+ type: 'tool_decision';
53
+ id: string;
54
+ decision: ToolDecision;
51
55
  } | {
52
56
  type: 'subagent_start';
53
57
  id: string;
@@ -7,6 +7,7 @@ import { fieldBox } from "../../../components/fieldBox.js";
7
7
  import { focusOnMount } from "../../../components/focus.js";
8
8
  import { Disclosure } from "../../../components/Disclosure.js";
9
9
  import { formatDuration } from "../../../format.js";
10
+ import { RelativeTime } from "../../../components/relativeTime.js";
10
11
  import { LogViewer } from "../../../logviewer/LogViewer.js";
11
12
  import { agentColorHex, bashExitCode, fullToolArgs, isGroupableTool, rollupSentence, summarizeToolArgs, toolHeaderFor, toolSummary } from "../../agent/toolfmt.js";
12
13
  import { computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, parseEditArgs } from "../../agent/diff.js";
@@ -277,10 +278,54 @@ function ToolCallBlock({ part }) {
277
278
  isDiff && /* @__PURE__ */ jsx("div", {
278
279
  className: "ml-5",
279
280
  children: /* @__PURE__ */ jsx(EditDiffBlock, { part })
280
- })
281
+ }),
282
+ part.decision && /* @__PURE__ */ jsx(DecisionLine, { decision: part.decision })
281
283
  ]
282
284
  });
283
285
  }
286
+ function decisionLabel(d, t) {
287
+ switch (d.status) {
288
+ case "approved":
289
+ if (d.by === "auto-reviewer") return t.allowedByReviewer;
290
+ return d.name ? t.allowedBy(d.name) : t.allowed;
291
+ case "denied":
292
+ if (d.by === "auto-reviewer") return t.deniedByReviewer;
293
+ return d.name ? t.deniedBy(d.name) : t.denied;
294
+ case "expired": return t.approvalExpired;
295
+ case "timed_out": return d.by === "auto-reviewer" ? t.reviewTimedOut : t.approvalTimedOut;
296
+ case "failed": return d.by === "auto-reviewer" ? t.reviewFailed : t.denied;
297
+ }
298
+ }
299
+ function DecisionLine({ decision }) {
300
+ const { strings } = useChatConfig();
301
+ return /* @__PURE__ */ jsxs("div", {
302
+ className: "ml-5 mt-0.5 text-xs theme-muted-text",
303
+ "data-testid": "tool-decision",
304
+ children: [/* @__PURE__ */ jsxs("div", {
305
+ className: "flex min-w-0 flex-wrap items-baseline gap-x-1",
306
+ children: [
307
+ /* @__PURE__ */ jsx("span", { children: decisionLabel(decision, strings.agentTranscript) }),
308
+ decision.at && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
309
+ "aria-hidden": "true",
310
+ children: "·"
311
+ }), /* @__PURE__ */ jsx(RelativeTime, {
312
+ value: decision.at,
313
+ className: "shrink-0"
314
+ })] }),
315
+ decision.note && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
316
+ "aria-hidden": "true",
317
+ children: "·"
318
+ }), /* @__PURE__ */ jsx("span", {
319
+ className: "min-w-0 max-w-full truncate",
320
+ children: decision.note
321
+ })] })
322
+ ]
323
+ }), decision.reason && /* @__PURE__ */ jsx("p", {
324
+ className: "mt-0.5 line-clamp-2",
325
+ children: decision.reason
326
+ })]
327
+ });
328
+ }
284
329
  /** Drawn the way the TUI draws a delegated Task: a tool-shaped header
285
330
  * `Task(description)`, and under it the latest action while running or
286
331
  * `Done (summary)` once finished. Expanding shows the whole activity feed. */
@@ -749,7 +794,7 @@ function buildRenderItems(parts) {
749
794
  run = [];
750
795
  };
751
796
  parts.forEach((part, index) => {
752
- if (part.kind === "reasoning" || part.kind === "tool_call" && part.status === "ok" && isGroupableTool(part.name)) {
797
+ if (part.kind === "reasoning" || part.kind === "tool_call" && part.status === "ok" && !part.decision && isGroupableTool(part.name)) {
753
798
  run.push({
754
799
  part,
755
800
  index
@@ -39,6 +39,10 @@ function applyPartDelta(parts, delta) {
39
39
  status: delta.isError ? "error" : "ok",
40
40
  result: delta.result
41
41
  } : p);
42
+ case "tool_decision": return parts.map((p) => p.kind === "tool_call" && p.id === delta.id ? {
43
+ ...p,
44
+ decision: delta.decision
45
+ } : p);
42
46
  case "subagent_start": {
43
47
  const part = {
44
48
  kind: "subagent",
@@ -234,6 +234,17 @@ export interface ChatStrings {
234
234
  denyWithFeedback: string;
235
235
  feedbackPlaceholder: string;
236
236
  answered: string;
237
+ /** Under a call someone answered: who allowed or denied it. */
238
+ allowedBy: (name: string) => string;
239
+ deniedBy: (name: string) => string;
240
+ allowed: string;
241
+ denied: string;
242
+ allowedByReviewer: string;
243
+ deniedByReviewer: string;
244
+ approvalExpired: string;
245
+ approvalTimedOut: string;
246
+ reviewTimedOut: string;
247
+ reviewFailed: string;
237
248
  writeOwnAnswer: string;
238
249
  subagent: string;
239
250
  tasks: string;
@@ -253,6 +253,16 @@ var defaultChatStrings = {
253
253
  denyWithFeedback: "No, and provide feedback to the agent",
254
254
  feedbackPlaceholder: "Tell the agent what to do instead…",
255
255
  answered: "Answered",
256
+ allowedBy: (name) => `Allowed by ${name}`,
257
+ deniedBy: (name) => `Denied by ${name}`,
258
+ allowed: "Allowed",
259
+ denied: "Denied",
260
+ allowedByReviewer: "Allowed by the auto-reviewer",
261
+ deniedByReviewer: "Denied by the auto-reviewer",
262
+ approvalExpired: "Expired without an answer",
263
+ approvalTimedOut: "Denied: no answer in time",
264
+ reviewTimedOut: "Denied: the auto-reviewer timed out",
265
+ reviewFailed: "Denied: the auto-reviewer failed",
256
266
  writeOwnAnswer: "Write my own answer…",
257
267
  subagent: "Task",
258
268
  tasks: "Tasks",
@@ -55,6 +55,19 @@ export interface ReasoningPart {
55
55
  text: string;
56
56
  durationMs?: number;
57
57
  }
58
+ export type ToolDecisionStatus = 'approved' | 'denied' | 'expired' | 'timed_out' | 'failed';
59
+ /** How a call that needed an answer was settled. */
60
+ export interface ToolDecision {
61
+ status: ToolDecisionStatus;
62
+ by: 'user' | 'auto-reviewer';
63
+ /** The person who answered, when `by` is a user. */
64
+ name?: string;
65
+ /** ISO time of the answer. */
66
+ at?: string;
67
+ note?: string;
68
+ /** Why it was settled this way, such as the auto-reviewer's rationale. */
69
+ reason?: string;
70
+ }
58
71
  export interface ToolCallPart {
59
72
  kind: 'tool_call';
60
73
  id: string;
@@ -63,6 +76,7 @@ export interface ToolCallPart {
63
76
  status: ToolCallStatus;
64
77
  result?: string;
65
78
  startLine?: number;
79
+ decision?: ToolDecision;
66
80
  }
67
81
  export type SubagentStatus = 'running' | 'done' | 'failed';
68
82
  export interface SubagentPart {
@@ -268,6 +268,9 @@ export interface UIStrings {
268
268
  annotationSource: (job: string, step: string) => string;
269
269
  /** A step without a name, numbered from 0 as the run reports it. */
270
270
  stepNumber: (index: number) => string;
271
+ /** How many warnings: the title of a run's list of warnings, and the name of the mark on a job
272
+ * or step they're about, such as "2 warnings". */
273
+ warningCount: (count: number) => string;
271
274
  };
272
275
  jobActions: {
273
276
  openSubworkflowLogs: string;
@@ -281,7 +281,8 @@ var DEFAULTS = {
281
281
  ]
282
282
  ].filter(([count]) => count).map(([count, one, many]) => `${count} ${count === 1 ? one : many}`).join(", ")}`,
283
283
  annotationSource: (job, step) => `in ${job} → ${step}`,
284
- stepNumber: (index) => `step ${index}`
284
+ stepNumber: (index) => `step ${index}`,
285
+ warningCount: (count) => count === 1 ? "1 warning" : `${count} warnings`
285
286
  },
286
287
  jobActions: {
287
288
  openSubworkflowLogs: "Open subworkflow logs",
@@ -1,5 +1,6 @@
1
1
  import type { GraphLayout } from '../editing.js';
2
2
  import type { DependencyGraphEditor } from './editorApi.js';
3
+ import { type RunWarning } from './runWarnings.js';
3
4
  import { type RunLink, type WorkflowJob } from './types.js';
4
5
  export type ViewMode = 'dag' | 'tree';
5
6
  export declare const addCleanupSteps: ({ jobs }: {
@@ -16,6 +17,9 @@ interface GraphRun {
16
17
  completedAt?: string | undefined;
17
18
  executedJobs?: Record<string, unknown> | undefined;
18
19
  links?: Record<string, RunLink> | undefined;
20
+ /** What is probably wrong with the run's jobs and steps as written, listed over the graph and
21
+ * marked on each job and step they're about, which matters most for one skipped without a log. */
22
+ warnings?: readonly RunWarning[] | null | undefined;
19
23
  }
20
24
  export default function DependencyGraph({ run, preview, removeBorder, viewMode, setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar, }: {
21
25
  run: GraphRun;
@@ -14,12 +14,15 @@ import { useStickyTop } from "../components/stickyTop.js";
14
14
  import { Tabs } from "../components/Tabs.js";
15
15
  import { LogViewer } from "../logviewer/LogViewer.js";
16
16
  import { initializeValues } from "../form/lib.js";
17
+ import { prefersReducedMotion } from "../reducedMotion.js";
17
18
  import { AnnotationBanner } from "./AnnotationBanner.js";
18
19
  import { jobLabel, toggled } from "./util.js";
19
20
  import { EDITOR_HANDLE_CLASS, GraphEditorProvider, useGraphEditor, useSelectedEdges } from "./graphEditorState.js";
20
21
  import { EdgeEnds, EdgeHitPath, EmptyGraphEditor, GraphEditorCanvas, NodePorts } from "./editorRows.js";
21
22
  import { emptyColumnsStyle } from "./gridSpacing.js";
22
23
  import { Reveal } from "./Reveal.js";
24
+ import { RunWarningsList } from "./RunWarningsList.js";
25
+ import { WarningMarks, WarningMarksContext, locateWarnings, rowKey } from "./runWarnings.js";
23
26
  import { Joblist } from "./JobSummary.js";
24
27
  import { Store, useStore } from "./store.js";
25
28
  import { Lineage, LineageContext } from "./lineage.js";
@@ -32,7 +35,7 @@ import { MatrixGroupNode, MatrixGroupSummaryItem } from "./MatrixGroup.js";
32
35
  import { StepSummaries } from "./StepSummaries.js";
33
36
  import { StepSummary } from "./StepSummary.js";
34
37
  import TreeView from "./TreeView.js";
35
- import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, fitMargin, fitView, intoView, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
38
+ import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoView, viewShowing, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
36
39
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
37
40
  import { Fragment as Fragment$1, Suspense, createContext, lazy, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
38
41
  import { flushSync } from "react-dom";
@@ -224,6 +227,7 @@ function jobHandlers({ engine, jobs, displayJobs, pathPrefix, stepsOpen, toggleS
224
227
  const openLog = (j, s) => setSublogOpen(engine.stepLogPath(pathPrefix, j, s));
225
228
  const openSummary = (j, s) => setSublogOpen(fileBeside(engine.stepLogPath(pathPrefix, j, s), "summary.md"));
226
229
  return {
230
+ pathPrefix,
227
231
  isStepsOpen,
228
232
  onJobClick: (j) => {
229
233
  const status = jobs[j]?.status ?? displayJobs[j]?.status;
@@ -700,6 +704,43 @@ function graphBounds(content) {
700
704
  }
701
705
  return bounds;
702
706
  }
707
+ var READABLE_SCALE = .5;
708
+ /** Where a job or step a warning took the graph to shows: the panel below its view bar, which sits
709
+ * over the graph. */
710
+ function visibleArea(panel, bar) {
711
+ const outer = panel.getBoundingClientRect();
712
+ const top = outer.top + panel.clientTop;
713
+ const below = bar ? Math.max(top, bar.getBoundingClientRect().bottom) : top;
714
+ return {
715
+ left: outer.left + panel.clientLeft,
716
+ top: below,
717
+ width: panel.clientWidth,
718
+ height: Math.max(0, top + panel.clientHeight - below)
719
+ };
720
+ }
721
+ function flash(row) {
722
+ const tone = getComputedStyle(row).getPropertyValue("--theme-warning").trim();
723
+ if (!tone || typeof row.animate !== "function") return;
724
+ const ring = {
725
+ outline: `3px solid ${tone}`,
726
+ outlineOffset: "4px",
727
+ borderRadius: "6px"
728
+ };
729
+ row.animate([
730
+ ring,
731
+ {
732
+ ...ring,
733
+ offset: .7
734
+ },
735
+ {
736
+ ...ring,
737
+ outlineColor: "transparent"
738
+ }
739
+ ], {
740
+ duration: 2e3,
741
+ easing: "ease-in"
742
+ });
743
+ }
703
744
  function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar }) {
704
745
  const engine = useWorkflowEngine();
705
746
  const animT = .125;
@@ -708,6 +749,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
708
749
  const runSlug = run.slug ?? "";
709
750
  const workflowName = run.workflowName ?? "";
710
751
  const runStatus = toStatus(run.status);
752
+ const runWarnings = run.warnings;
753
+ const warnings = useMemo(() => runWarnings?.length ? locateWarnings(runWarnings, rootJobs) : [], [runWarnings, rootJobs]);
754
+ const freshMarks = useMemo(() => warnings.length ? new WarningMarks(warnings) : WarningMarks.EMPTY, [warnings]);
755
+ const [marks, setMarks] = useState(freshMarks);
756
+ if (marks !== freshMarks && marks.signature !== freshMarks.signature) setMarks(freshMarks);
711
757
  const [sublogOpen, setSublogOpen] = useState("");
712
758
  const [stepsOpen, setStepsOpen] = useState(/* @__PURE__ */ new Set());
713
759
  const [expandedSubworkflows, setExpandedSubworkflows] = useState(/* @__PURE__ */ new Set());
@@ -901,8 +947,14 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
901
947
  ]);
902
948
  const { dependencyCols } = rootGraph;
903
949
  const held = useRef(null);
950
+ const shown = useRef(null);
951
+ const viewBarRef = useRef(null);
904
952
  const viewed = useRef(false);
905
953
  const rowOf = useCallback((job) => [...containerRef.current?.querySelectorAll("[data-dag-job]") ?? []].find((el) => el.dataset["dagJob"] === job) ?? null, []);
954
+ const runRowOf = useCallback((key, jobKey) => {
955
+ const rows = [...containerRef.current?.querySelectorAll("[data-run-row]") ?? []];
956
+ return rows.find((el) => el.dataset["runRow"] === key) ?? (jobKey === void 0 ? null : rows.find((el) => el.dataset["runRow"] === jobKey) ?? null);
957
+ }, []);
906
958
  const nodeOf = useCallback((job) => {
907
959
  const row = rowOf(job);
908
960
  return row?.closest("[id^=\"node_\"]") ?? row;
@@ -927,6 +979,18 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
927
979
  const transform = transformRef.current;
928
980
  const bounds = content ? graphBounds(content) : null;
929
981
  if (!container || !content || !transform || !bounds) return;
982
+ const showing = shown.current && performance.now() - shown.current.at < HOLD_MS ? shown.current.keys.flatMap((key) => runRowOf(key) ?? []) : [];
983
+ const [row] = showing;
984
+ if (row) {
985
+ const { positionX, positionY, scale } = transform.instance.state;
986
+ const view = viewShowing(enclosing(showing.map((el) => el.getBoundingClientRect())), row.getBoundingClientRect(), visibleArea(container, viewBarRef.current), (transform.instance.wrapperComponent ?? container).getBoundingClientRect(), {
987
+ x: positionX,
988
+ y: positionY,
989
+ scale
990
+ }, READABLE_SCALE);
991
+ if (view) transform.setTransform(view.x, view.y, view.scale, animationMs, "easeOut");
992
+ return;
993
+ }
930
994
  const panel = {
931
995
  width: container.clientWidth,
932
996
  height: container.clientHeight
@@ -956,7 +1020,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
956
1020
  const wrapperEl = container.querySelector(".react-transform-component");
957
1021
  if (wrapperEl) wrapperEl.style.transform = `translate(${view.x}px, ${view.y}px) scale(${view.scale})`;
958
1022
  requestAnimationFrame(() => transform.setTransform(view.x, view.y, view.scale, 0));
959
- }, [nodeOf, editor]);
1023
+ }, [
1024
+ nodeOf,
1025
+ editor,
1026
+ runRowOf
1027
+ ]);
960
1028
  const fitWhenSettled = useCallback(async () => {
961
1029
  const container = containerRef.current;
962
1030
  const content = graphContentRef.current;
@@ -989,6 +1057,51 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
989
1057
  const pos = offsetWithin(el, content);
990
1058
  transform.setTransform(container.clientWidth / 2 - (pos.x + el.offsetWidth / 2) * scale, container.clientHeight / 2 - (pos.y + el.offsetHeight / 2) * scale, scale, FIT_ANIMATION_MS, "easeOut");
991
1059
  }, []);
1060
+ const canShowWarnings = viewMode !== "tree" || !!setViewMode;
1061
+ const showWarning = (target) => {
1062
+ const elsewhere = !target.prefix.startsWith(pathAppend);
1063
+ const via = elsewhere ? target.via : target.via.filter((v) => v.prefix.startsWith(pathAppend));
1064
+ const jobKeys = via.map((v) => rowKey(v.prefix, v.job));
1065
+ if (target.step !== void 0) jobKeys.push(rowKey(target.prefix, target.job));
1066
+ const matrixKeys = [...via, target].flatMap(({ prefix, job }) => {
1067
+ const origin = walkJobPath(rootJobs, parsePrefixToPath(prefix))[job]?._matrix?.originaljob;
1068
+ return origin ? [`${prefix}${origin}`] : [];
1069
+ });
1070
+ shown.current = {
1071
+ keys: [
1072
+ target.key,
1073
+ rowKey(target.prefix, target.job),
1074
+ ...via.flatMap((v) => [rowKey(v.prefix, v.job, v.step), rowKey(v.prefix, v.job)]).reverse()
1075
+ ],
1076
+ at: performance.now()
1077
+ };
1078
+ flushSync(() => {
1079
+ if (viewMode === "tree") setViewMode?.("dag");
1080
+ if (elsewhere) {
1081
+ resetGraph();
1082
+ setExecutedJobsPath((prev) => [[], ...prev.slice(executedJobsPathIdx, 10)]);
1083
+ setExecutedJobsPathIdx(0);
1084
+ }
1085
+ setStepsOpen((prev) => /* @__PURE__ */ new Set([...prev, ...jobKeys]));
1086
+ setExpandedSubworkflows((prev) => /* @__PURE__ */ new Set([...prev, ...via.map((v) => `${v.prefix}${v.job}:${v.step}`)]));
1087
+ setMatrixOpen((prev) => /* @__PURE__ */ new Set([...prev, ...matrixKeys]));
1088
+ });
1089
+ const row = runRowOf(target.key, rowKey(target.prefix, target.job));
1090
+ if (row) {
1091
+ row.focus({ preventScroll: true });
1092
+ flash(row);
1093
+ }
1094
+ const first = fitViewRef.current;
1095
+ if (first && first.scale < READABLE_SCALE) fitViewRef.current = {
1096
+ ...first,
1097
+ scale: READABLE_SCALE
1098
+ };
1099
+ containerRef.current?.scrollIntoView({
1100
+ block: "nearest",
1101
+ behavior: prefersReducedMotion() ? "auto" : "smooth"
1102
+ });
1103
+ fitWhenSettled();
1104
+ };
992
1105
  const registerShow = editor?.registerShow;
993
1106
  useEffect(() => {
994
1107
  if (!editorApi) return;
@@ -1363,6 +1476,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1363
1476
  })
1364
1477
  ]
1365
1478
  }),
1479
+ !preview && /* @__PURE__ */ jsx(RunWarningsList, {
1480
+ warnings,
1481
+ rootJobs,
1482
+ onShow: canShowWarnings ? showWarning : void 0
1483
+ }),
1366
1484
  !preview && /* @__PURE__ */ jsx(AnnotationBanner, { executedJobs: jobs }),
1367
1485
  viewMode === "tree" && !preview ? /* @__PURE__ */ jsx("div", {
1368
1486
  className: "panel w-full border",
@@ -1396,6 +1514,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1396
1514
  onPinch: moved,
1397
1515
  onTransform,
1398
1516
  children: () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
1517
+ ref: viewBarRef,
1399
1518
  className: "absolute z-10 top-2 right-2 flex max-w-[calc(100%-1rem)] flex-col items-end gap-y-1",
1400
1519
  children: /* @__PURE__ */ jsx(GraphViewControls, {
1401
1520
  history: executedJobsPath.length > 1 ? {
@@ -1498,7 +1617,10 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1498
1617
  });
1499
1618
  return /* @__PURE__ */ jsx(GraphEditorProvider, {
1500
1619
  value: editorApi,
1501
- children: graph
1620
+ children: /* @__PURE__ */ jsx(WarningMarksContext, {
1621
+ value: marks,
1622
+ children: graph
1623
+ })
1502
1624
  });
1503
1625
  }
1504
1626
  /** Jobs as a run evaluates them before it starts: each `strategy`, and a `needs` expression, from the inputs' defaults. */
@@ -7,8 +7,8 @@ import { menuDivider } from "../components/MenuEntries.js";
7
7
  import { IconButton } from "../components/IconButton.js";
8
8
  import { useContextMenu } from "../components/ContextMenu.js";
9
9
  import { NumberInput } from "../components/NumberInput.js";
10
- import { MarqueeBox, createStore, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore } from "./editorPrimitives.js";
11
10
  import { prefersReducedMotion } from "../reducedMotion.js";
11
+ import { MarqueeBox, createStore, reclaimFocus, returningFocus, trackDrag, useFocusAfterDialog, usePressOutside, useStore } from "./editorPrimitives.js";
12
12
  import { useGraphEditorStrings, useInputsEditorStrings } from "./editorStrings.js";
13
13
  import { asRecord } from "./records.js";
14
14
  import { openOnAddOf, text } from "./editorFields.js";
@@ -2,6 +2,8 @@ import type { ReactNode } from 'react';
2
2
  import type { WorkflowJob } from './types.js';
3
3
  /** What a job list does when its jobs and steps are clicked, at one graph level. */
4
4
  export interface JobHandlers {
5
+ /** The level's `subworkflows/<job>/step_<n>/` chain, empty at the run's own jobs. */
6
+ pathPrefix: string;
5
7
  isStepsOpen: (jobName: string) => boolean;
6
8
  onJobClick: (jobName: string) => void;
7
9
  onStepClick: (jobName: string, stepIndex: number) => void;
@@ -6,6 +6,8 @@ import { RunStatusIcon } from "../components/RunStatusIcon.js";
6
6
  import { jobLabel } from "./util.js";
7
7
  import { AddStepButton, EditableJobRow, EditableStepRow } from "./editorRows.js";
8
8
  import { Reveal } from "./Reveal.js";
9
+ import { WarningMark } from "./RunWarningsList.js";
10
+ import { rowKey, useWarningMarks } from "./runWarnings.js";
9
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
12
  //#region src/graph/JobSummary.tsx
11
13
  function UsesIcon({ uses, subworkflow }) {
@@ -32,6 +34,8 @@ function UsesIcon({ uses, subworkflow }) {
32
34
  function Joblist(inputs) {
33
35
  const { dag, jobActions } = useStrings();
34
36
  const engine = useWorkflowEngine();
37
+ const marks = useWarningMarks();
38
+ const prefix = inputs.pathPrefix;
35
39
  return inputs.jobNames.map((jobName) => {
36
40
  if (inputs.jobs[jobName]?._matrixGroup) return null;
37
41
  const job = inputs.jobs[jobName];
@@ -40,12 +44,17 @@ function Joblist(inputs) {
40
44
  const toggle = /* @__PURE__ */ jsxs("button", {
41
45
  type: "button",
42
46
  "aria-expanded": inputs.isStepsOpen(jobName),
47
+ "data-run-row": rowKey(prefix, jobName),
43
48
  onClick: () => inputs.onJobClick(jobName),
44
49
  className: cx("flex items-center gap-x-1.5 w-full text-left", jobStatus === "skipped" || jobStatus === "skipped-failed" ? "cursor-default" : "cursor-pointer"),
45
- children: [!inputs.preview && /* @__PURE__ */ jsx(RunStatusIcon, { status: jobStatus }), /* @__PURE__ */ jsx("div", {
46
- className: "font-medium",
47
- children: label
48
- })]
50
+ children: [
51
+ !inputs.preview && /* @__PURE__ */ jsx(RunStatusIcon, { status: jobStatus }),
52
+ /* @__PURE__ */ jsx("div", {
53
+ className: "font-medium",
54
+ children: label
55
+ }),
56
+ /* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName)) })
57
+ ]
49
58
  });
50
59
  return /* @__PURE__ */ jsxs("div", {
51
60
  className: "py-1 select-none",
@@ -65,10 +74,15 @@ function Joblist(inputs) {
65
74
  const canExpand = !!subJobs && Object.keys(subJobs).length > 0;
66
75
  const stepButton = /* @__PURE__ */ jsxs("button", {
67
76
  type: "button",
77
+ "data-run-row": rowKey(prefix, jobName, i),
68
78
  onClick: () => inputs.onStepClick(jobName, i),
69
79
  className: cx(inputs.preview ? "ml-1.5" : "ml-4", "gap-x-1.5 flex items-center text-left"),
70
80
  "data-testid": stepLabel,
71
- children: [inputs.preview ? "↳" : /* @__PURE__ */ jsx(RunStatusIcon, { status: step.status }), /* @__PURE__ */ jsx("div", { children: stepLabel })]
81
+ children: [
82
+ inputs.preview ? "↳" : /* @__PURE__ */ jsx(RunStatusIcon, { status: step.status }),
83
+ /* @__PURE__ */ jsx("div", { children: stepLabel }),
84
+ /* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName, i)) })
85
+ ]
72
86
  });
73
87
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
74
88
  className: cx("flex items-center", step.status === "skipped" || step.status === "skipped-failed" ? "cursor-default" : "cursor-pointer"),
@@ -2,14 +2,21 @@ import { useStrings, useWorkflowEngine } from "../components/Provider.js";
2
2
  import { RunStatusIcon } from "../components/RunStatusIcon.js";
3
3
  import { EditableJobRow } from "./editorRows.js";
4
4
  import { Reveal } from "./Reveal.js";
5
+ import { WarningMark } from "./RunWarningsList.js";
6
+ import { rowKey, useWarningMarks } from "./runWarnings.js";
5
7
  import { Joblist } from "./JobSummary.js";
6
8
  import { GraphNode } from "./GraphNode.js";
7
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
10
  import { memo } from "react";
9
11
  //#region src/graph/MatrixGroup.tsx
12
+ /** The warnings about any of a matrix's runs, for its group's row while it is closed. */
13
+ function useMatrixWarnings(prefix, members) {
14
+ return useWarningMarks().underAny(members.map((member) => rowKey(prefix, member)));
15
+ }
10
16
  /** Matrix group item in the summary sidebar. */
11
17
  function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, jobs, handlers, preview }) {
12
18
  const { dag: t } = useStrings();
19
+ const warnings = useMatrixWarnings(handlers.pathPrefix, matrixGroup.members);
13
20
  return /* @__PURE__ */ jsxs("div", {
14
21
  className: "py-1.5",
15
22
  children: [/* @__PURE__ */ jsxs("button", {
@@ -17,17 +24,21 @@ function MatrixGroupSummaryItem({ matrixGroup, status, isExpanded, onToggle, job
17
24
  "aria-expanded": isExpanded,
18
25
  onClick: onToggle,
19
26
  className: "cursor-pointer flex items-center gap-x-1.5 w-full text-left",
20
- children: [/* @__PURE__ */ jsx(RunStatusIcon, { status }), /* @__PURE__ */ jsxs("div", {
21
- className: "font-semibold",
22
- children: [t.matrixOf(matrixGroup.originaljob), /* @__PURE__ */ jsxs("span", {
23
- className: "font-normal text-sm ml-1",
24
- children: [
25
- "(",
26
- t.jobCount(matrixGroup.members.length),
27
- ")"
28
- ]
29
- })]
30
- })]
27
+ children: [
28
+ /* @__PURE__ */ jsx(RunStatusIcon, { status }),
29
+ /* @__PURE__ */ jsxs("div", {
30
+ className: "font-semibold",
31
+ children: [t.matrixOf(matrixGroup.originaljob), /* @__PURE__ */ jsxs("span", {
32
+ className: "font-normal text-sm ml-1",
33
+ children: [
34
+ "(",
35
+ t.jobCount(matrixGroup.members.length),
36
+ ")"
37
+ ]
38
+ })]
39
+ }),
40
+ !isExpanded && /* @__PURE__ */ jsx(WarningMark, { warnings })
41
+ ]
31
42
  }), /* @__PURE__ */ jsx(Reveal, {
32
43
  open: isExpanded,
33
44
  children: /* @__PURE__ */ jsx("div", {
@@ -49,6 +60,7 @@ var MatrixGroupNode = memo(function MatrixGroupNode({ matrixGroup, jobNames, idP
49
60
  const first = jobNames[0] ?? "";
50
61
  const matrixMembers = matrixGroup.members;
51
62
  const { aggStatus, statusLabel } = useWorkflowEngine().matrixStatus(matrixMembers.map((m) => jobs[m]?.status));
63
+ const warnings = useMatrixWarnings(idPrefix, matrixMembers);
52
64
  const header = /* @__PURE__ */ jsxs("button", {
53
65
  type: "button",
54
66
  "aria-expanded": isExpanded,
@@ -65,7 +77,11 @@ var MatrixGroupNode = memo(function MatrixGroupNode({ matrixGroup, jobNames, idP
65
77
  })] }) : t.matrixOf(matrixName)
66
78
  }), !isExpanded && /* @__PURE__ */ jsxs("div", {
67
79
  className: "mt-1 border-t border-(--theme-border) pt-1 flex items-center gap-x-1",
68
- children: [!preview && /* @__PURE__ */ jsx(RunStatusIcon, { status: aggStatus }), /* @__PURE__ */ jsx("span", { children: preview ? t.jobCount(matrixMembers.length) : statusLabel })]
80
+ children: [
81
+ !preview && /* @__PURE__ */ jsx(RunStatusIcon, { status: aggStatus }),
82
+ /* @__PURE__ */ jsx("span", { children: preview ? t.jobCount(matrixMembers.length) : statusLabel }),
83
+ /* @__PURE__ */ jsx(WarningMark, { warnings })
84
+ ]
69
85
  })]
70
86
  });
71
87
  return /* @__PURE__ */ jsxs(GraphNode, {
@@ -0,0 +1,19 @@
1
+ import type { LocatedWarning, RunWarning, WarningTarget } from './runWarnings.js';
2
+ import type { WorkflowJob } from './types.js';
3
+ /** A warning's message, with the text it puts in backticks, such as a keyword, set as code. */
4
+ export declare function WarningText({ message }: {
5
+ message: string;
6
+ }): string | (string | import("react").JSX.Element)[];
7
+ /** Flags a job or step that warnings are about, or that holds something they're about; hovering
8
+ * it lists them. */
9
+ export declare function WarningMark({ warnings, className, }: {
10
+ warnings: readonly RunWarning[];
11
+ className?: string | undefined;
12
+ }): import("react").JSX.Element | null;
13
+ /** The run's warnings, each with a link to the job or step it's about. */
14
+ export declare function RunWarningsList({ warnings, rootJobs, onShow, }: {
15
+ warnings: readonly LocatedWarning[];
16
+ rootJobs: Record<string, WorkflowJob>;
17
+ /** Brings a warning's job or step into view; without it, where each one is reads as text. */
18
+ onShow?: ((target: WarningTarget) => void) | undefined;
19
+ }): import("react").JSX.Element | null;