@shipfox/client-workflows 16.0.0 → 18.0.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 (182) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/dist/components/identifier/identifier.js +2 -2
  4. package/dist/components/identifier/identifier.js.map +1 -1
  5. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  6. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  8. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  9. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  10. package/dist/components/job-detail/job-context-panel.js +228 -0
  11. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  12. package/dist/components/job-detail/job-detail-header.d.ts +3 -1
  13. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  14. package/dist/components/job-detail/job-detail-header.js +54 -48
  15. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  16. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  17. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.js +100 -57
  19. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  20. package/dist/components/job-detail/job-empty-states.d.ts +4 -0
  21. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.js +60 -11
  23. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  24. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  25. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  26. package/dist/components/job-detail/json-code.d.ts +14 -0
  27. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  28. package/dist/components/job-detail/json-code.js +93 -0
  29. package/dist/components/job-detail/json-code.js.map +1 -0
  30. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  31. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  32. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  33. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  34. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  35. package/dist/components/job-detail/step-troubleshooting.js +497 -0
  36. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  37. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  38. package/dist/components/job-graph/job-graph-content.js +5 -4
  39. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  40. package/dist/components/job-graph/job-node.js +3 -3
  41. package/dist/components/job-graph/job-node.js.map +1 -1
  42. package/dist/components/step-list/index.d.ts +1 -0
  43. package/dist/components/step-list/index.d.ts.map +1 -1
  44. package/dist/components/step-list/index.js.map +1 -1
  45. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  46. package/dist/components/step-list/step-list-model.js +6 -0
  47. package/dist/components/step-list/step-list-model.js.map +1 -1
  48. package/dist/components/step-list/step-list.d.ts +5 -1
  49. package/dist/components/step-list/step-list.d.ts.map +1 -1
  50. package/dist/components/step-list/step-list.js +117 -82
  51. package/dist/components/step-list/step-list.js.map +1 -1
  52. package/dist/components/workflow-run-duration-label.js +1 -1
  53. package/dist/components/workflow-run-duration-label.js.map +1 -1
  54. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  55. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  56. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  59. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  65. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  66. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.js +8 -13
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  72. package/dist/components/workflow-run-tabs/index.d.ts +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  74. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  75. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +2 -2
  76. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +9 -2
  78. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -1
  79. package/dist/components/workflow-run-tabs/run-annotation-list.js +28 -19
  80. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -1
  81. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  82. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +5 -5
  83. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  84. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  85. package/dist/components/workflow-run-view/run-workspace-nav.js +11 -10
  86. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  87. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  88. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  90. package/dist/components/workflow-run-view/workflow-run-view.js +72 -12
  91. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  92. package/dist/core/entities/job-execution.d.ts +9 -0
  93. package/dist/core/entities/job-execution.d.ts.map +1 -1
  94. package/dist/core/entities/job-execution.js.map +1 -1
  95. package/dist/core/entities/job.d.ts +10 -1
  96. package/dist/core/entities/job.d.ts.map +1 -1
  97. package/dist/core/entities/job.js.map +1 -1
  98. package/dist/core/entities/step-attempt.d.ts +29 -0
  99. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  100. package/dist/core/entities/step-attempt.js.map +1 -1
  101. package/dist/core/entities/step.d.ts +3 -1
  102. package/dist/core/entities/step.d.ts.map +1 -1
  103. package/dist/core/entities/step.js.map +1 -1
  104. package/dist/core/run-annotation.d.ts +6 -0
  105. package/dist/core/run-annotation.d.ts.map +1 -1
  106. package/dist/core/run-annotation.js.map +1 -1
  107. package/dist/core/workflow-run.d.ts +1 -1
  108. package/dist/core/workflow-run.d.ts.map +1 -1
  109. package/dist/core/workflow-run.js.map +1 -1
  110. package/dist/hooks/api/annotations.d.ts +63 -0
  111. package/dist/hooks/api/annotations.d.ts.map +1 -0
  112. package/dist/hooks/api/annotations.js +145 -0
  113. package/dist/hooks/api/annotations.js.map +1 -0
  114. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  115. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  116. package/dist/hooks/api/step-attempt-detail.js +43 -0
  117. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  118. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  119. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  120. package/dist/hooks/api/workflow-run-mapper.js +48 -0
  121. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  122. package/dist/pages/project-workflows-page.js +19 -19
  123. package/dist/pages/project-workflows-page.js.map +1 -1
  124. package/dist/pages/workflow-run-list-page.js +1 -1
  125. package/dist/pages/workflow-run-list-page.js.map +1 -1
  126. package/dist/tsconfig.test.tsbuildinfo +1 -1
  127. package/package.json +10 -10
  128. package/src/components/identifier/identifier.tsx +2 -2
  129. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  130. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  131. package/src/components/job-detail/job-context-panel.tsx +241 -0
  132. package/src/components/job-detail/job-detail-header.tsx +52 -46
  133. package/src/components/job-detail/job-detail-view.stories.tsx +555 -53
  134. package/src/components/job-detail/job-detail-view.tsx +105 -59
  135. package/src/components/job-detail/job-empty-states.test.ts +105 -1
  136. package/src/components/job-detail/job-empty-states.tsx +77 -10
  137. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  138. package/src/components/job-detail/json-code.tsx +120 -0
  139. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  140. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  141. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  142. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  143. package/src/components/job-detail/step-troubleshooting.tsx +541 -0
  144. package/src/components/job-graph/job-graph-content.tsx +4 -3
  145. package/src/components/job-graph/job-node.tsx +3 -3
  146. package/src/components/step-list/index.ts +1 -0
  147. package/src/components/step-list/step-list-model.ts +6 -0
  148. package/src/components/step-list/step-list.test.tsx +75 -2
  149. package/src/components/step-list/step-list.tsx +73 -20
  150. package/src/components/workflow-run-duration-label.tsx +1 -1
  151. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  152. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  153. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  154. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  155. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  156. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  157. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  158. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  159. package/src/components/workflow-run-summary/workflow-run-summary.tsx +11 -11
  160. package/src/components/workflow-run-tabs/index.ts +5 -1
  161. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +2 -2
  162. package/src/components/workflow-run-tabs/run-annotation-list.tsx +25 -10
  163. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +5 -5
  164. package/src/components/workflow-run-view/run-workspace-nav.tsx +18 -11
  165. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  166. package/src/components/workflow-run-view/workflow-run-view.tsx +114 -14
  167. package/src/core/entities/job-execution.ts +9 -0
  168. package/src/core/entities/job.ts +12 -1
  169. package/src/core/entities/step-attempt.ts +33 -0
  170. package/src/core/entities/step.ts +3 -1
  171. package/src/core/run-annotation.ts +6 -0
  172. package/src/core/workflow-run.ts +4 -0
  173. package/src/hooks/api/annotations.test.ts +84 -0
  174. package/src/hooks/api/annotations.ts +240 -0
  175. package/src/hooks/api/step-attempt-detail.ts +67 -0
  176. package/src/hooks/api/workflow-run-mapper.ts +47 -0
  177. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  178. package/src/pages/project-workflows-page.tsx +19 -19
  179. package/src/pages/workflow-job-detail-page.test.tsx +52 -1
  180. package/src/pages/workflow-run-list-page.tsx +1 -1
  181. package/test/fixtures/workflow-run.ts +7 -0
  182. package/tsconfig.build.tsbuildinfo +1 -1
@@ -9,9 +9,11 @@ import {Skeleton} from '@shipfox/react-ui/skeleton';
9
9
  import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
10
10
  import {Text} from '@shipfox/react-ui/typography';
11
11
  import {Link} from '@tanstack/react-router';
12
- import {type RefObject, useEffect, useRef} from 'react';
12
+ import {type RefObject, useEffect, useRef, useState} from 'react';
13
+ import type {RunAnnotationSummary} from '#core/run-annotation.js';
13
14
  import {summarizeJobAnnotations} from '#core/run-annotation.js';
14
- import type {Job, JobExecution} from '#core/workflow-run.js';
15
+ import {isWorkflowRunTerminal, type Job, type JobExecution} from '#core/workflow-run.js';
16
+ import {useWorkflowRunAnnotationSummaryQuery} from '#hooks/api/annotations.js';
15
17
  import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
16
18
  import type {useWorkflowRunAttemptQuery} from '#hooks/api/workflow-runs.js';
17
19
  import {
@@ -24,15 +26,14 @@ import {
24
26
  WorkflowRunNotFound,
25
27
  WorkflowRunStaleError,
26
28
  } from '../workflow-run-view/workflow-run-states.js';
27
- import {AgentConfigFailureCallout} from './agent-config-failure-callout.js';
29
+ import {JobContextPanel} from './job-context-panel.js';
28
30
  import {JobDetailHeader} from './job-detail-header.js';
29
31
  import {
30
32
  CarriedOverStepPanel,
31
33
  emptyStateForJob,
32
34
  emptyStateForMissingExecution,
33
- isAgentConfigFailure,
34
35
  jobSucceededSummary,
35
- toSelectedAttemptError,
36
+ MaterializedOutputFailureNotice,
36
37
  } from './job-empty-states.js';
37
38
  import {
38
39
  resolveWorkflowJobSelection,
@@ -40,6 +41,9 @@ import {
40
41
  workflowJobLandingSelection,
41
42
  } from './job-selection.js';
42
43
  import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
44
+ import {StepInspectorSheet} from './step-troubleshooting.js';
45
+
46
+ type InspectorState = {key: string; attemptId: string | null};
43
47
 
44
48
  export interface JobDetailViewProps {
45
49
  workspaceSlug: string;
@@ -68,7 +72,8 @@ export function JobDetailView({
68
72
  const pageScrollRef = useRef<HTMLDivElement>(null);
69
73
  const landingSelectionRef = useRef<FrozenLandingSelection | undefined>(undefined);
70
74
  const hasLoadedData = query.data !== undefined;
71
- // Same query key as the run workspace, so this reads the cache instead of fetching again.
75
+ // Reuse the run workspace's bounded annotation read for the job header chip. The separate
76
+ // summary query below stays counts-only and is scoped to the inspector's selected execution.
72
77
  const annotations = useRunAnnotationsQuery({
73
78
  workflowRunId,
74
79
  runAttempt: query.data?.runAttempt.attempt,
@@ -78,6 +83,26 @@ export function JobDetailView({
78
83
  truncated: annotations.summary?.truncated ?? false,
79
84
  })
80
85
  : undefined;
86
+ const inspectorResetKey = `${jobId}:${search.jobExecutionId ?? ''}`;
87
+ const [inspectorState, setInspectorState] = useState<InspectorState>(() => ({
88
+ key: inspectorResetKey,
89
+ attemptId: null,
90
+ }));
91
+ const inspectorOpenAttemptId =
92
+ inspectorState.key === inspectorResetKey ? inspectorState.attemptId : null;
93
+ const annotationJob = query.data?.jobs.find((candidate) => candidate.id === jobId);
94
+ const annotationExecutionId = annotationJob
95
+ ? resolveWorkflowJobSelection({job: annotationJob, selection: search}).jobExecution?.id
96
+ : search.jobExecutionId;
97
+ const annotationPolling = query.data
98
+ ? !isWorkflowRunTerminal(query.data.runAttempt.status)
99
+ : true;
100
+ const annotationSummaryQuery = useWorkflowRunAnnotationSummaryQuery(
101
+ query.data?.id,
102
+ query.data?.runAttempt.attempt,
103
+ annotationExecutionId,
104
+ {polling: annotationPolling},
105
+ );
81
106
 
82
107
  useEffect(() => {
83
108
  if (!jobId || !hasLoadedData) return;
@@ -185,6 +210,10 @@ export function JobDetailView({
185
210
  });
186
211
  }
187
212
 
213
+ function onInspectorOpenChange(attemptId: string | null) {
214
+ setInspectorState({key: inspectorResetKey, attemptId});
215
+ }
216
+
188
217
  function retargetToRunningStep() {
189
218
  if (!runningSelection || !selectedJobExecution) return;
190
219
  onSelectionChange({
@@ -208,11 +237,8 @@ export function JobDetailView({
208
237
  attempt={newerAttempt}
209
238
  />
210
239
  ) : null}
211
- <div ref={pageScrollRef} className="@container min-h-0 flex-1 overflow-auto pb-24">
212
- <section
213
- aria-label={`${job.displayName} logs`}
214
- className="mx-auto flex w-full max-w-[1120px] flex-col gap-8 px-24"
215
- >
240
+ <div ref={pageScrollRef} className="@container min-h-0 flex-1 overflow-auto pb-panel">
241
+ <section aria-label={`${job.displayName} logs`} className="flex w-full flex-col">
216
242
  <section className="min-w-0 overflow-hidden">
217
243
  <JobDetailHeader
218
244
  job={job}
@@ -220,32 +246,55 @@ export function JobDetailView({
220
246
  onSelectedJobExecutionChange={selectExecution}
221
247
  workspaceSlug={workspaceSlug}
222
248
  projectSlug={projectSlug}
223
- workflowRunId={workflowRunId}
249
+ workflowRunId={run.id}
224
250
  runAttempt={run.runAttempt.attempt}
225
251
  annotationSummary={jobAnnotationSummary}
252
+ jobContext={
253
+ selectedJobExecution ? (
254
+ <JobContextPanel job={job} execution={selectedJobExecution} />
255
+ ) : undefined
256
+ }
226
257
  />
227
- <Text as="h3" className="sr-only">
258
+ <Text as="h2" className="sr-only">
228
259
  Logs
229
260
  </Text>
230
261
  {selectedJobExecution ? (
231
- <StepList
232
- job={job}
233
- jobExecution={selectedJobExecution}
234
- selectedAttemptId={selectedAttemptId}
235
- defaultSelectedAttemptId={landingSelection?.attemptId}
236
- onSelectedAttemptChange={selectAttempt}
237
- autoSelectActiveAttempt
238
- emptyState={emptyStateForJob(job, selectedJobExecution)}
239
- showHeader={false}
240
- className="rounded-none border-0"
241
- renderExpandedStep={(context) => (
242
- <ExpandedStep
243
- context={context}
244
- workspaceSlug={workspaceSlug}
245
- pageScrollRef={pageScrollRef}
246
- />
247
- )}
248
- />
262
+ <>
263
+ <MaterializedOutputFailureNotice jobExecution={selectedJobExecution} />
264
+ <StepList
265
+ job={job}
266
+ jobExecution={selectedJobExecution}
267
+ selectedAttemptId={selectedAttemptId}
268
+ defaultSelectedAttemptId={landingSelection?.attemptId}
269
+ onSelectedAttemptChange={selectAttempt}
270
+ inspectorOpenAttemptId={inspectorOpenAttemptId}
271
+ onInspectorOpenChange={onInspectorOpenChange}
272
+ autoSelectActiveAttempt
273
+ emptyState={emptyStateForJob(job, selectedJobExecution)}
274
+ showHeader={false}
275
+ className="rounded-none border-0 bg-transparent"
276
+ renderExpandedStep={(context) => (
277
+ <ExpandedStep context={context} pageScrollRef={pageScrollRef} />
278
+ )}
279
+ renderInspector={(entry) => (
280
+ <StepInspectorSheet
281
+ entry={entry}
282
+ open
283
+ onOpenChange={(open) => onInspectorOpenChange(open ? entry.id : null)}
284
+ workspaceSlug={workspaceSlug}
285
+ projectSlug={projectSlug}
286
+ workflowRunId={run.id}
287
+ runAttempt={run.runAttempt.attempt}
288
+ jobId={job.id}
289
+ annotationCount={annotationCountForStep(
290
+ annotationSummaryQuery.data,
291
+ entry.step.id,
292
+ entry.attempt,
293
+ )}
294
+ />
295
+ )}
296
+ />
297
+ </>
249
298
  ) : (
250
299
  <EmptyStateForMissingExecution job={job} />
251
300
  )}
@@ -254,7 +303,7 @@ export function JobDetailView({
254
303
  <div
255
304
  role="status"
256
305
  aria-live="polite"
257
- className="flex min-w-0 items-center justify-between gap-8 rounded-8 border border-border-neutral-base bg-background-components-base px-10 py-8"
306
+ className="flex min-w-0 items-center justify-between gap-inline border-t border-border-neutral-base px-row py-row"
258
307
  >
259
308
  <Text size="xs" className="min-w-0 text-foreground-neutral-muted">
260
309
  Run moved on to{' '}
@@ -265,7 +314,7 @@ export function JobDetailView({
265
314
  </Text>
266
315
  <button
267
316
  type="button"
268
- className="shrink-0 rounded-4 px-6 py-4 text-xs font-medium text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
317
+ className="shrink-0 rounded-4 px-tight py-[4px] text-xs font-medium text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
269
318
  onClick={retargetToRunningStep}
270
319
  >
271
320
  Jump to it
@@ -273,7 +322,7 @@ export function JobDetailView({
273
322
  </div>
274
323
  ) : null}
275
324
  {succeededSummary ? (
276
- <Text size="xs" className="px-8 text-foreground-neutral-muted">
325
+ <Text size="xs" className="px-row py-row text-foreground-neutral-muted">
277
326
  {succeededSummary}
278
327
  </Text>
279
328
  ) : null}
@@ -286,39 +335,27 @@ export function JobDetailView({
286
335
 
287
336
  function ExpandedStep({
288
337
  context,
289
- workspaceSlug,
290
338
  pageScrollRef,
291
339
  }: {
292
340
  context: StepExpandedContext;
293
- workspaceSlug: string;
294
341
  pageScrollRef: RefObject<HTMLDivElement | null>;
295
342
  }) {
296
343
  if (context.carriedOver) return <CarriedOverStepPanel />;
297
344
 
298
- const selectedAttemptError =
299
- toSelectedAttemptError(context.step, context.attemptError) ?? context.step.error;
300
-
301
345
  return (
302
346
  <section
303
347
  role="region"
304
348
  tabIndex={0}
305
349
  aria-label={`${context.stepLabel} output, attempt ${context.attempt}`}
306
- className="flex min-w-0 flex-col gap-10 border-t border-border-neutral-base bg-background-neutral-subtle px-16 py-12 outline-none focus-visible:shadow-border-interactive-with-active"
350
+ className="flex min-w-0 flex-col border-t border-border-neutral-base bg-background-neutral-base outline-none focus-visible:shadow-border-interactive-with-active dark:bg-background-contrast-subtle"
307
351
  >
308
- {isAgentConfigFailure(context.step, selectedAttemptError) ? (
309
- <AgentConfigFailureCallout
310
- workspaceSlug={workspaceSlug}
311
- config={context.step.agentConfig}
312
- error={selectedAttemptError}
313
- />
314
- ) : null}
315
352
  <StepAttemptLogPanel
316
353
  stepId={context.stepId}
317
354
  attempt={context.attempt}
318
355
  attemptStatus={context.attemptStatus}
319
356
  attemptStartedAt={context.attemptStartedAt}
320
357
  pageScrollRef={pageScrollRef}
321
- surfaceClassName="rounded-8"
358
+ surfaceClassName="rounded-none border-0 bg-transparent shadow-none"
322
359
  />
323
360
  </section>
324
361
  );
@@ -328,7 +365,7 @@ function EmptyStateForMissingExecution({job}: {job: Job}) {
328
365
  const emptyState = emptyStateForMissingExecution(job);
329
366
  return (
330
367
  <EmptyState
331
- className="min-h-120 px-16 py-20"
368
+ className="min-h-120 p-panel"
332
369
  icon="componentLine"
333
370
  title={emptyState.title}
334
371
  description={emptyState.description}
@@ -349,7 +386,7 @@ function JobNotFoundState({
349
386
  search: WorkflowJobSearch;
350
387
  }) {
351
388
  return (
352
- <div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-12 p-24">
389
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-cluster p-panel">
353
390
  <EmptyState
354
391
  icon="componentLine"
355
392
  title="Job not found"
@@ -364,7 +401,7 @@ function JobNotFoundState({
364
401
  {runAttempt: search.runAttempt},
365
402
  ) as never
366
403
  }
367
- className="inline-flex items-center gap-6 rounded-6 px-8 py-6 text-sm text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
404
+ className="inline-flex items-center gap-inline rounded-6 px-tight py-[6px] text-sm text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
368
405
  >
369
406
  <Icon name="arrowLeftLine" size={14} aria-hidden="true" />
370
407
  Back to run summary
@@ -387,8 +424,8 @@ function NewerAttemptNotice({
387
424
  attempt: number;
388
425
  }) {
389
426
  return (
390
- <div role="status" className="border-b border-border-neutral-base px-16 py-8">
391
- <div className="mx-auto flex max-w-[1120px] items-center justify-between gap-8 rounded-8 border border-border-neutral-base bg-background-components-base px-10 py-8">
427
+ <div role="status" className="border-b border-border-neutral-base px-row py-row">
428
+ <div className="flex items-center justify-between gap-inline">
392
429
  <Text size="xs" className="text-foreground-neutral-muted">
393
430
  A newer run attempt is available.
394
431
  </Text>
@@ -396,7 +433,7 @@ function NewerAttemptNotice({
396
433
  to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId"
397
434
  params={{workspaceSlug, projectSlug, workflowRunId: runId, jobId}}
398
435
  search={workflowJobSearchParams({runAttempt: attempt}) as never}
399
- className="shrink-0 rounded-4 px-6 py-4 text-xs font-medium text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
436
+ className="shrink-0 rounded-4 px-tight py-[4px] text-xs font-medium text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
400
437
  >
401
438
  View attempt #{attempt}
402
439
  </Link>
@@ -431,21 +468,30 @@ function findAttempt(jobExecution: JobExecution, attemptId: string) {
431
468
  return undefined;
432
469
  }
433
470
 
471
+ function annotationCountForStep(
472
+ summary: RunAnnotationSummary | undefined,
473
+ stepId: string,
474
+ attempt: number,
475
+ ): number | undefined {
476
+ return summary?.stepCounts?.find((entry) => entry.stepId === stepId && entry.attempt === attempt)
477
+ ?.total;
478
+ }
479
+
434
480
  function JobDetailSkeleton() {
435
481
  return (
436
- <div className="min-h-0 min-w-0 flex-1 overflow-auto px-24 pb-24">
437
- <div className="mx-auto max-w-[1120px] overflow-hidden">
438
- <header className="flex items-center gap-12 border-b border-border-neutral-base px-16 py-12">
482
+ <div className="min-h-0 min-w-0 flex-1 overflow-auto pb-panel">
483
+ <div className="w-full">
484
+ <header className="flex items-center gap-cluster border-b border-border-neutral-base px-row py-row">
439
485
  <Skeleton className="size-20 rounded-full" />
440
486
  <Skeleton className="h-20 w-160 rounded-4" />
441
487
  <Skeleton className="h-24 w-72 rounded-6" />
442
488
  <Skeleton className="h-24 w-180 rounded-4" />
443
489
  </header>
444
- <div className="p-16">
490
+ <div className="p-panel-compact">
445
491
  {JOB_DETAIL_SKELETON_ROWS.map((row) => (
446
492
  <div
447
493
  key={row}
448
- className="flex min-h-44 items-center gap-8 border-b border-border-neutral-base last:border-b-0"
494
+ className="flex min-h-44 items-center gap-inline border-b border-border-neutral-base last:border-b-0"
449
495
  >
450
496
  <Skeleton className="size-14 rounded-full" />
451
497
  <Skeleton className="h-16 w-180 rounded-4" />
@@ -3,7 +3,13 @@ import {
3
3
  workflowJobExecutionDto,
4
4
  workflowStepDto,
5
5
  } from '#test/fixtures/workflow-run.js';
6
- import {jobSucceededSummary} from './job-empty-states.js';
6
+ import {
7
+ emptyStateForJob,
8
+ emptyStateForMissingExecution,
9
+ jobSucceededSummary,
10
+ outputFailureDescriptionForExecution,
11
+ skippedJobDescription,
12
+ } from './job-empty-states.js';
7
13
 
8
14
  describe('jobSucceededSummary', () => {
9
15
  test('counts only succeeded steps when skipped steps are present', () => {
@@ -25,3 +31,101 @@ describe('jobSucceededSummary', () => {
25
31
  expect(jobSucceededSummary(job, execution)).toBe('1 step succeeded');
26
32
  });
27
33
  });
34
+
35
+ describe('skippedJobDescription', () => {
36
+ test('explains when materialized output exceeds the configured size limit', () => {
37
+ expect(skippedJobDescription('output_too_large')).toBe(
38
+ 'The materialized job output exceeded its configured size limit.',
39
+ );
40
+ });
41
+ });
42
+
43
+ describe('materialized output failure descriptions', () => {
44
+ const fallback =
45
+ 'A materialized job output could not be persisted: it exceeded a size or entry cap, contained a non-JSON-safe value, or referenced an unresolved value. Check the output mapping and values before re-running the workflow.';
46
+
47
+ test('uses the server-authored message for an execution with recorded steps', () => {
48
+ const job = workflowJob({
49
+ status: 'failed',
50
+ status_reason: 'output_invalid',
51
+ job_executions: [
52
+ workflowJobExecutionDto({
53
+ status: 'failed',
54
+ status_reason: 'output_invalid',
55
+ status_reason_message: 'Job output "payload" cannot be persisted as JSON: undefined.',
56
+ steps: [workflowStepDto({status: 'succeeded'})],
57
+ }),
58
+ ],
59
+ });
60
+ const execution = job.jobExecutions[0];
61
+ if (!execution) throw new Error('Expected a job execution');
62
+
63
+ expect(outputFailureDescriptionForExecution(execution)).toBe(
64
+ 'Job output "payload" cannot be persisted as JSON: undefined.',
65
+ );
66
+ });
67
+
68
+ test('keeps the generic fallback for old output-invalid rows without a message', () => {
69
+ const job = workflowJob({
70
+ status: 'failed',
71
+ status_reason: 'output_invalid',
72
+ job_executions: [
73
+ workflowJobExecutionDto({
74
+ status: 'failed',
75
+ status_reason: 'output_invalid',
76
+ steps: [workflowStepDto({status: 'succeeded'})],
77
+ }),
78
+ ],
79
+ });
80
+ const execution = job.jobExecutions[0];
81
+ if (!execution) throw new Error('Expected a job execution');
82
+
83
+ expect(outputFailureDescriptionForExecution(execution)).toBe(fallback);
84
+ });
85
+
86
+ test('uses the message in the empty state when output materialization failed before steps', () => {
87
+ const job = workflowJob({
88
+ status: 'failed',
89
+ status_reason: 'output_invalid',
90
+ job_executions: [
91
+ workflowJobExecutionDto({
92
+ status: 'failed',
93
+ status_reason: 'output_invalid',
94
+ status_reason_message: 'Job outputs cannot define more than 10 entries (found 11)',
95
+ steps: [],
96
+ }),
97
+ ],
98
+ });
99
+ const execution = job.jobExecutions[0];
100
+ if (!execution) throw new Error('Expected a job execution');
101
+
102
+ expect(emptyStateForJob(job, execution)).toMatchObject({
103
+ description: 'Job outputs cannot define more than 10 entries (found 11)',
104
+ });
105
+ });
106
+
107
+ test('keeps output failures scoped to the selected execution', () => {
108
+ const job = workflowJob({
109
+ status: 'failed',
110
+ status_reason: 'output_invalid',
111
+ job_executions: [
112
+ workflowJobExecutionDto({
113
+ status: 'succeeded',
114
+ status_reason: null,
115
+ steps: [workflowStepDto({status: 'succeeded'})],
116
+ }),
117
+ ],
118
+ });
119
+ const execution = job.jobExecutions[0];
120
+ if (!execution) throw new Error('Expected a job execution');
121
+
122
+ expect(outputFailureDescriptionForExecution(execution)).toBeUndefined();
123
+ expect(
124
+ emptyStateForMissingExecution(
125
+ workflowJob({status: 'failed', status_reason: 'output_invalid'}),
126
+ ),
127
+ ).toMatchObject({
128
+ description: fallback,
129
+ });
130
+ });
131
+ });
@@ -1,3 +1,4 @@
1
+ import {Callout, CalloutContent, CalloutDescription, CalloutTitle} from '@shipfox/react-ui/callout';
1
2
  import {EmptyState} from '@shipfox/react-ui/empty-state';
2
3
  import type {Job, JobExecution, Step, StepError} from '#core/workflow-run.js';
3
4
  import {
@@ -9,6 +10,44 @@ import {
9
10
  import type {StepListEmptyState} from '../step-list/index.js';
10
11
  import {formatJobExecutionTime} from './job-execution-time-text.js';
11
12
 
13
+ const MATERIALIZED_OUTPUT_FAILURE_DESCRIPTION =
14
+ 'A materialized job output could not be persisted: it exceeded a size or entry cap, contained a non-JSON-safe value, or referenced an unresolved value. Check the output mapping and values before re-running the workflow.';
15
+ const OUTPUT_TOO_LARGE_FAILURE_DESCRIPTION =
16
+ 'The materialized job output exceeded its configured size limit. Review the failure details before re-running the workflow.';
17
+
18
+ export function outputFailureDescriptionForExecution(
19
+ jobExecution: JobExecution,
20
+ ): string | undefined {
21
+ if (jobExecution.steps.length === 0) return undefined;
22
+
23
+ if (jobExecution.statusReason === 'output_invalid') {
24
+ return jobExecution.statusReasonMessage || MATERIALIZED_OUTPUT_FAILURE_DESCRIPTION;
25
+ }
26
+ if (jobExecution.statusReason === 'output_too_large') {
27
+ return jobExecution.statusReasonMessage || OUTPUT_TOO_LARGE_FAILURE_DESCRIPTION;
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ export function MaterializedOutputFailureNotice({jobExecution}: {jobExecution: JobExecution}) {
33
+ const description = outputFailureDescriptionForExecution(jobExecution);
34
+ if (!description) return null;
35
+
36
+ return (
37
+ <Callout
38
+ role="alert"
39
+ type="error"
40
+ variant="secondary"
41
+ className="border-b border-border-neutral-base px-row py-row"
42
+ >
43
+ <CalloutContent>
44
+ <CalloutTitle>Job output could not be persisted</CalloutTitle>
45
+ <CalloutDescription>{description}</CalloutDescription>
46
+ </CalloutContent>
47
+ </Callout>
48
+ );
49
+ }
50
+
12
51
  export function emptyStateForJob(
13
52
  job: Job,
14
53
  jobExecution: JobExecution,
@@ -25,11 +64,14 @@ export function emptyStateForJob(
25
64
  job.mode === 'listening'
26
65
  ? deriveJobExecutionDisplayStatus(jobExecution)
27
66
  : deriveJobDisplayStatus(job);
67
+ const runner = jobExecution.runner?.length ? jobExecution.runner : job.runner;
28
68
 
29
69
  if (jobExecution.status === 'running' && displayStatus === 'pending') {
30
70
  return {
31
- title: 'Runner preparing job',
32
- description: 'A runner is assigned. Steps will appear here when work begins.',
71
+ title: runner?.length ? 'Runner preparing job' : 'Waiting for a runner',
72
+ description: runner?.length
73
+ ? `Runner ${runner.join(', ')} is preparing the job. Steps will appear here when work begins.`
74
+ : 'No runner has claimed this job yet. Steps will appear here when a runner starts work.',
33
75
  status: displayStatus,
34
76
  };
35
77
  }
@@ -61,7 +103,11 @@ export function emptyStateForJob(
61
103
  if (displayStatus === 'failed') {
62
104
  return {
63
105
  title: 'Job failed before its first step started',
64
- description: preStepFailureDescription(jobExecution.statusReason ?? job.statusReason),
106
+ description: preStepFailureDescription(
107
+ jobExecution.statusReason ?? job.statusReason,
108
+ runner,
109
+ jobExecution.statusReasonMessage,
110
+ ),
65
111
  status: displayStatus,
66
112
  };
67
113
  }
@@ -126,6 +172,14 @@ export function emptyStateForMissingExecution(job: Job): StepListEmptyState {
126
172
  };
127
173
  }
128
174
 
175
+ if (job.status === 'failed') {
176
+ return {
177
+ title: 'Job failed before an execution was created',
178
+ description: preStepFailureDescription(job.statusReason, job.runner),
179
+ status: 'failed',
180
+ };
181
+ }
182
+
129
183
  return {
130
184
  title: 'Execution details unavailable',
131
185
  description: 'This job finished, but no job execution record is available.',
@@ -148,24 +202,37 @@ export function skippedJobDescription(reason: Job['statusReason']): string {
148
202
  case 'run_cancelled':
149
203
  case 'timed_out':
150
204
  case 'runner_lost':
205
+ case 'output_invalid':
151
206
  case 'step_failed':
152
207
  case 'unknown':
153
208
  case null:
154
209
  return 'This job did not start.';
210
+ case 'output_too_large':
211
+ return 'The materialized job output exceeded its configured size limit.';
155
212
  }
156
213
  }
157
214
 
158
- function preStepFailureDescription(reason: string | null): string {
215
+ function preStepFailureDescription(
216
+ reason: string | null,
217
+ runner: string[] | null = null,
218
+ statusReasonMessage: string | null = null,
219
+ ): string {
220
+ const runnerCopy = runner?.length ? ` Required runner labels: ${runner.join(', ')}.` : '';
221
+
159
222
  switch (reason) {
160
223
  case 'runner_lost':
161
- return 'The runner stopped responding before work began. Check runner availability before re-running the workflow.';
224
+ return `The runner stopped responding before work began.${runnerCopy} Check runner availability before re-running the workflow.`;
162
225
  case 'timed_out':
163
- return 'The job timed out before work began. Check runner capacity and timeout configuration before re-running the workflow.';
226
+ return `The job timed out before work began.${runnerCopy} Check runner capacity and timeout configuration before re-running the workflow.`;
164
227
  case 'user_cancelled':
165
228
  case 'run_cancelled':
166
229
  return 'The execution ended while the run was being cancelled.';
167
230
  case 'step_failed':
168
- return 'The execution failed before step details were recorded. Review run annotations before re-running the workflow.';
231
+ return `The execution failed before step details were recorded.${runnerCopy} Review run annotations before re-running the workflow.`;
232
+ case 'output_too_large':
233
+ return statusReasonMessage || OUTPUT_TOO_LARGE_FAILURE_DESCRIPTION;
234
+ case 'output_invalid':
235
+ return statusReasonMessage || MATERIALIZED_OUTPUT_FAILURE_DESCRIPTION;
169
236
  case 'condition_errored':
170
237
  return 'The job condition could not be evaluated. Review run annotations before re-running the workflow.';
171
238
  case 'dependency_not_completed':
@@ -176,9 +243,9 @@ function preStepFailureDescription(reason: string | null): string {
176
243
  return 'The job condition did not match, so no work started.';
177
244
  case 'unknown':
178
245
  case null:
179
- return 'Shipfox did not record a failure reason. Re-run only after checking the runner and workflow configuration.';
246
+ return `Shipfox did not record a failure reason.${runnerCopy} Re-run only after checking the runner and workflow configuration.`;
180
247
  default:
181
- return `The execution ended because ${humanizeFailureReason(reason)}. Resolve that condition before re-running the workflow.`;
248
+ return `The execution ended because ${humanizeFailureReason(reason)}.${runnerCopy} Resolve that condition before re-running the workflow.`;
182
249
  }
183
250
  }
184
251
 
@@ -189,7 +256,7 @@ function humanizeFailureReason(reason: string): string {
189
256
  export function CarriedOverStepPanel() {
190
257
  return (
191
258
  <EmptyState
192
- className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base px-16 py-20"
259
+ className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base p-panel"
193
260
  icon="componentLine"
194
261
  title="Carried over from a previous attempt"
195
262
  description="Not executed in this run."
@@ -44,7 +44,7 @@ export function JobExecutionSwitcher({
44
44
  return (
45
45
  <div
46
46
  className={cn(
47
- 'flex min-w-0 items-center gap-6 text-sm leading-20 text-foreground-neutral-subtle',
47
+ 'flex min-w-0 items-center gap-inline text-sm leading-20 text-foreground-neutral-subtle',
48
48
  className,
49
49
  )}
50
50
  >
@@ -59,8 +59,8 @@ export function JobExecutionSwitcher({
59
59
  className={cn(
60
60
  'inline-flex min-w-0 max-w-full items-center rounded-6 text-left transition-colors focus-visible:shadow-border-interactive-with-active focus-visible:outline-none',
61
61
  variant === 'title'
62
- ? '-mx-6 -my-4 gap-8 px-6 py-4 hover:bg-background-components-hover'
63
- : 'min-h-28 gap-6 px-8 py-4 text-sm leading-20 text-foreground-neutral-subtle hover:bg-background-components-hover',
62
+ ? '-mx-inline -my-[4px] gap-inline px-tight py-[4px] hover:bg-background-components-hover'
63
+ : 'min-h-28 gap-inline px-tight py-[4px] text-sm leading-20 text-foreground-neutral-subtle hover:bg-background-components-hover',
64
64
  className,
65
65
  )}
66
66
  aria-label={`Switch job execution, currently execution ${selected.sequence}: ${selected.displayName}`}
@@ -141,7 +141,7 @@ function executionAccessibleLabel(execution: JobExecution): string {
141
141
 
142
142
  function TitleExecutionSummary({execution}: {execution: JobExecution}) {
143
143
  return (
144
- <span className="flex min-w-0 items-center gap-8">
144
+ <span className="flex min-w-0 items-center gap-inline">
145
145
  <Text as="span" size="sm" bold className="shrink-0 text-foreground-neutral-base">
146
146
  #{execution.sequence}
147
147
  </Text>
@@ -156,7 +156,7 @@ function ExecutionSummary({execution}: {execution: JobExecution}) {
156
156
  const displayStatus = deriveJobExecutionDisplayStatus(execution);
157
157
 
158
158
  return (
159
- <span className="flex min-w-0 items-center gap-6">
159
+ <span className="flex min-w-0 items-center gap-inline">
160
160
  <WorkflowStatusIcon status={displayStatus} size={14} tooltip={false} />
161
161
  <span className="shrink-0 font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
162
162
  Execution #{execution.sequence}