@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
@@ -1,5 +1,9 @@
1
1
  export {RunAnnotationCountChip} from './run-annotation-count-chip.js';
2
2
  export {annotationElementId, RunAnnotationItem} from './run-annotation-item.js';
3
- export {RunAnnotationList, type RunAnnotationListQuery} from './run-annotation-list.js';
3
+ export {
4
+ type DerivedRunAnnotation,
5
+ RunAnnotationList,
6
+ type RunAnnotationListQuery,
7
+ } from './run-annotation-list.js';
4
8
  export {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
5
9
  export {RunAnnotationsEmpty} from './run-annotations-empty.js';
@@ -45,10 +45,10 @@ export function RunAnnotationCountChip({
45
45
  search={workflowRunSearchParams(search, search) as never}
46
46
  aria-label={accessibleLabel}
47
47
  className={cn(
48
- 'inline-flex h-20 shrink-0 items-center gap-4 rounded-4 border px-6 font-code text-xs leading-16 tabular-nums outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active',
48
+ 'inline-flex h-20 shrink-0 items-center gap-tight rounded-4 border px-tight font-code text-xs leading-16 tabular-nums outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active',
49
49
  // The chip stays 20px optically so it sits level with the duration metadata beside it;
50
50
  // the hit area grows to the 44px minimum only where the pointer is coarse.
51
- '[@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:px-10',
51
+ '[@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:px-[10px]',
52
52
  severity ? SEVERITY_CHIP_TONE[severity] : 'border-border-neutral-base',
53
53
  )}
54
54
  >
@@ -1,11 +1,11 @@
1
- import {QueryLoadError, type QueryLoadErrorQuery} from '@shipfox/client-ui';
1
+ import {AnnotationCard, QueryLoadError, type QueryLoadErrorQuery} from '@shipfox/client-ui';
2
2
  import {Button} from '@shipfox/react-ui/button';
3
3
  import {Callout} from '@shipfox/react-ui/callout';
4
4
  import {EmptyState} from '@shipfox/react-ui/empty-state';
5
5
  import {Skeleton} from '@shipfox/react-ui/skeleton';
6
6
  import {Text} from '@shipfox/react-ui/typography';
7
7
  import {useState} from 'react';
8
- import type {RunAnnotationEntry} from '#core/run-annotation.js';
8
+ import type {RunAnnotationEntry, RunAnnotationStyle} from '#core/run-annotation.js';
9
9
  import {RunAnnotationItem} from './run-annotation-item.js';
10
10
  import {RunAnnotationsEmpty} from './run-annotations-empty.js';
11
11
 
@@ -19,6 +19,8 @@ export interface RunAnnotationListQuery extends QueryLoadErrorQuery {
19
19
  export interface RunAnnotationListProps {
20
20
  query: RunAnnotationListQuery;
21
21
  entries: RunAnnotationEntry[] | undefined;
22
+ /** Synthetic terminal-job diagnostics for jobs that never created an execution record. */
23
+ derivedAnnotations?: readonly DerivedRunAnnotation[] | undefined;
22
24
  workflowRunId: string;
23
25
  workspaceSlug?: string | undefined;
24
26
  projectSlug?: string | undefined;
@@ -33,6 +35,12 @@ export interface RunAnnotationListProps {
33
35
  selectedAnnotationId?: string | undefined;
34
36
  }
35
37
 
38
+ export interface DerivedRunAnnotation {
39
+ id: string;
40
+ style: RunAnnotationStyle;
41
+ body: string;
42
+ }
43
+
36
44
  /**
37
45
  * How many annotations render at once.
38
46
  *
@@ -51,6 +59,7 @@ const RENDER_WINDOW = 25;
51
59
  export function RunAnnotationList({
52
60
  query,
53
61
  entries,
62
+ derivedAnnotations = [],
54
63
  workflowRunId,
55
64
  workspaceSlug,
56
65
  projectSlug,
@@ -80,12 +89,13 @@ export function RunAnnotationList({
80
89
 
81
90
  const visible = entries.slice(0, renderCount);
82
91
  const hiddenCount = entries.length - visible.length;
92
+ const hasContent = entries.length > 0 || derivedAnnotations.length > 0;
83
93
 
84
94
  return (
85
- <div className="flex min-w-0 flex-col gap-12">
95
+ <div className="flex min-w-0 flex-col gap-cluster">
86
96
  {query.isError ? <RunAnnotationStaleError query={query} /> : null}
87
97
 
88
- {entries.length === 0 ? (
98
+ {!hasContent ? (
89
99
  filtered ? (
90
100
  <RunAnnotationsFilteredEmpty
91
101
  jobName={filteredJobName}
@@ -97,7 +107,7 @@ export function RunAnnotationList({
97
107
  <RunAnnotationsEmpty />
98
108
  )
99
109
  ) : (
100
- <ol className="flex min-w-0 flex-col gap-12">
110
+ <ol className="flex min-w-0 flex-col gap-cluster">
101
111
  {visible.map((entry) => (
102
112
  <RunAnnotationItem
103
113
  key={entry.annotation.id}
@@ -109,6 +119,11 @@ export function RunAnnotationList({
109
119
  selected={entry.annotation.id === selectedAnnotationId}
110
120
  />
111
121
  ))}
122
+ {derivedAnnotations.map((annotation) => (
123
+ <li key={annotation.id}>
124
+ <AnnotationCard style={annotation.style} body={annotation.body} />
125
+ </li>
126
+ ))}
112
127
  </ol>
113
128
  )}
114
129
 
@@ -187,7 +202,7 @@ function RunAnnotationsFilteredEmpty({
187
202
  onClearFilters: (() => void) | undefined;
188
203
  }) {
189
204
  return (
190
- <div className="flex flex-col items-center gap-12">
205
+ <div className="flex flex-col items-center gap-cluster">
191
206
  <EmptyState
192
207
  icon="fileDamageLine"
193
208
  title={incomplete ? 'No matches in loaded annotations' : 'No matching annotations'}
@@ -216,7 +231,7 @@ function RunAnnotationsFilteredEmpty({
216
231
  function RunAnnotationStaleError({query}: {query: QueryLoadErrorQuery}) {
217
232
  return (
218
233
  <Callout role="status" aria-live="polite" type="error">
219
- <div className="flex w-full items-center justify-between gap-8">
234
+ <div className="flex w-full items-center justify-between gap-inline">
220
235
  <Text size="xs">Could not refresh annotations.</Text>
221
236
  <Button
222
237
  type="button"
@@ -239,14 +254,14 @@ const ANNOTATION_SKELETON_ROWS = ['first', 'second', 'third'];
239
254
 
240
255
  function RunAnnotationListSkeleton() {
241
256
  return (
242
- <section aria-label="Loading annotations" className="flex flex-col gap-12">
257
+ <section aria-label="Loading annotations" className="flex flex-col gap-cluster">
243
258
  {ANNOTATION_SKELETON_ROWS.map((row) => (
244
259
  <div
245
260
  key={row}
246
- className="flex gap-12 rounded-8 border border-border-neutral-base bg-background-components-base px-12 py-8"
261
+ className="flex gap-cluster rounded-8 border border-border-neutral-base bg-background-components-base px-[12px] py-[8px]"
247
262
  >
248
263
  <Skeleton className="h-40 w-4 rounded-full" />
249
- <div className="flex min-w-0 flex-1 flex-col gap-6">
264
+ <div className="flex min-w-0 flex-1 flex-col gap-inline">
250
265
  <Skeleton className="h-20 w-160 rounded-4" />
251
266
  <Skeleton className="h-16 w-240 rounded-4" />
252
267
  <Skeleton className="h-40 w-full rounded-4" />
@@ -1,3 +1,4 @@
1
+ import {MetadataSeparator} from '@shipfox/client-ui';
1
2
  import {Icon} from '@shipfox/react-ui/icon';
2
3
  import {Text} from '@shipfox/react-ui/typography';
3
4
  import {cn} from '@shipfox/react-ui/utils';
@@ -8,7 +9,6 @@ import {
8
9
  type WorkflowRunsSearch,
9
10
  workflowRunSearchParams,
10
11
  } from '#routes/inputs.js';
11
- import {MetadataSeparator} from '../workflow-run-summary/workflow-run-summary.js';
12
12
  import {SEVERITY_ICON, SEVERITY_ICON_TONE} from './severity-visuals.js';
13
13
 
14
14
  /**
@@ -48,7 +48,7 @@ export function RunAnnotationSummaryLine({
48
48
  const totalLabel = countLabel(summary.total, 'annotation', summary.truncated);
49
49
 
50
50
  return (
51
- <div className="flex flex-wrap items-center gap-x-8 gap-y-4 text-foreground-neutral-subtle">
51
+ <div className="flex flex-wrap items-center gap-x-inline gap-y-tight text-foreground-neutral-subtle">
52
52
  {search.severity && workspaceSlug && projectSlug && workflowRunId ? (
53
53
  // The severity filter has no control of its own, so while one is active the total is
54
54
  // the way back out. Unfiltered it stays plain text: a link to where you already are
@@ -77,7 +77,7 @@ export function RunAnnotationSummaryLine({
77
77
  </Text>
78
78
  )}
79
79
  {visibleSeverities.map((severity) => (
80
- <span key={severity} className="inline-flex shrink-0 items-center gap-8">
80
+ <span key={severity} className="inline-flex shrink-0 items-center gap-inline">
81
81
  <MetadataSeparator />
82
82
  <SeverityLink
83
83
  count={summary[severity]}
@@ -127,7 +127,7 @@ function SeverityLink({
127
127
  );
128
128
 
129
129
  if (!workspaceSlug || !projectSlug || !workflowRunId) {
130
- return <span className="inline-flex items-center gap-4">{content}</span>;
130
+ return <span className="inline-flex items-center gap-tight">{content}</span>;
131
131
  }
132
132
 
133
133
  const searchWithoutAnnotation = withoutAnnotation(search);
@@ -142,7 +142,7 @@ function SeverityLink({
142
142
  search,
143
143
  ) as never
144
144
  }
145
- className="inline-flex items-center gap-4 rounded-4 outline-none hover:underline focus-visible:shadow-border-interactive-with-active"
145
+ className="inline-flex items-center gap-tight rounded-4 outline-none hover:underline focus-visible:shadow-border-interactive-with-active"
146
146
  >
147
147
  {content}
148
148
  </Link>
@@ -53,12 +53,16 @@ export function RunWorkspaceNav({
53
53
 
54
54
  return (
55
55
  <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
56
- <aside className="w-full shrink-0 border-b border-border-neutral-base bg-background-neutral-background min-[768px]:w-240 min-[768px]:border-b-0 min-[768px]:border-r">
57
- <Collapsible open={mobileOpen} onOpenChange={setMobileOpen}>
56
+ <aside className="flex min-h-0 w-full shrink-0 flex-col border-b border-border-neutral-base bg-background-neutral-background min-[768px]:w-240 min-[768px]:border-b-0 min-[768px]:border-r">
57
+ <Collapsible
58
+ open={mobileOpen}
59
+ onOpenChange={setMobileOpen}
60
+ className="flex min-h-0 flex-col min-[768px]:flex-1"
61
+ >
58
62
  <CollapsibleTrigger asChild>
59
63
  <button
60
64
  type="button"
61
- className="flex min-h-44 w-full items-center justify-between gap-12 px-16 text-left outline-none focus-visible:shadow-border-interactive-with-active min-[768px]:hidden"
65
+ className="flex min-h-44 w-full items-center justify-between gap-cluster px-row text-left outline-none focus-visible:shadow-border-interactive-with-active min-[768px]:hidden"
62
66
  aria-label="Toggle run navigation"
63
67
  >
64
68
  <span className="min-w-0">
@@ -82,7 +86,7 @@ export function RunWorkspaceNav({
82
86
  </CollapsibleTrigger>
83
87
  <div
84
88
  className={cn(
85
- 'max-h-[50vh] overflow-y-auto p-8 min-[768px]:block min-[768px]:max-h-none min-[768px]:p-12',
89
+ 'flex min-h-0 max-h-[50vh] flex-col overflow-y-auto p-tight scrollbar min-[768px]:min-h-0 min-[768px]:max-h-none min-[768px]:flex-1 min-[768px]:flex min-[768px]:overflow-hidden min-[768px]:p-[12px]',
86
90
  !mobileOpen && 'hidden',
87
91
  )}
88
92
  >
@@ -136,7 +140,7 @@ function RunWorkspaceNavContent({
136
140
  }, [currentJobId, mobileOpen]);
137
141
 
138
142
  return (
139
- <nav aria-label="Run workspace" className="flex min-w-0 flex-col gap-16">
143
+ <nav aria-label="Run workspace" className="flex min-h-0 min-w-0 flex-1 flex-col gap-group">
140
144
  <ul>
141
145
  <li>
142
146
  <RunSectionLink
@@ -151,8 +155,11 @@ function RunWorkspaceNavContent({
151
155
  </li>
152
156
  </ul>
153
157
 
154
- <section aria-labelledby="run-workspace-jobs-heading" className="min-w-0">
155
- <div className="flex items-center justify-between gap-8 px-8 pb-6">
158
+ <section
159
+ aria-labelledby="run-workspace-jobs-heading"
160
+ className="flex min-h-0 min-w-0 flex-1 flex-col"
161
+ >
162
+ <div className="flex items-center justify-between gap-inline px-tight pb-[6px]">
156
163
  <Text
157
164
  as="h2"
158
165
  id="run-workspace-jobs-heading"
@@ -166,7 +173,7 @@ function RunWorkspaceNavContent({
166
173
  {jobs.length}
167
174
  </Text>
168
175
  </div>
169
- <ol className="max-h-[320px] overflow-y-auto">
176
+ <ol className="min-h-0 flex-1 overflow-y-auto scrollbar">
170
177
  {jobs.map((job) => {
171
178
  const current = job.id === currentJobId;
172
179
  const execution = defaultJobExecution(job);
@@ -182,7 +189,7 @@ function RunWorkspaceNavContent({
182
189
  onClick={onNavigate}
183
190
  aria-current={current ? 'page' : undefined}
184
191
  className={cn(
185
- 'flex min-h-32 min-w-0 items-center gap-8 rounded-4 px-8 text-left outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
192
+ 'flex min-h-32 min-w-0 items-center gap-inline rounded-4 px-tight text-left outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
186
193
  current && 'bg-background-neutral-hover',
187
194
  )}
188
195
  >
@@ -210,7 +217,7 @@ function RunWorkspaceNavContent({
210
217
  id="run-workspace-details-heading"
211
218
  size="xs"
212
219
  bold
213
- className="px-8 pb-6 text-foreground-neutral-muted"
220
+ className="px-tight pb-[6px] text-foreground-neutral-muted"
214
221
  >
215
222
  Run details
216
223
  </Text>
@@ -285,7 +292,7 @@ function RunSectionLink({
285
292
  : undefined
286
293
  }
287
294
  className={cn(
288
- 'flex min-h-32 items-center gap-8 rounded-4 px-8 text-xs font-medium text-foreground-neutral-base outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
295
+ 'flex min-h-32 items-center gap-inline rounded-4 px-tight text-xs font-medium text-foreground-neutral-base outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
289
296
  current && 'bg-background-neutral-hover',
290
297
  )}
291
298
  >
@@ -9,10 +9,10 @@ export function WorkflowRunSkeleton() {
9
9
  return (
10
10
  <section
11
11
  aria-label="Loading workflow run"
12
- className="bg-background-neutral-background px-16 py-12"
12
+ className="bg-background-neutral-background px-row py-row"
13
13
  >
14
- <div className="flex min-w-0 flex-wrap items-center gap-x-12 gap-y-8">
15
- <div className="flex min-w-0 items-center gap-8">
14
+ <div className="flex min-w-0 flex-wrap items-center gap-x-cluster gap-y-inline">
15
+ <div className="flex min-w-0 items-center gap-inline">
16
16
  <Skeleton className="size-8 rounded-full" />
17
17
  <Skeleton className="h-24 w-180 rounded-6" />
18
18
  </div>
@@ -35,7 +35,7 @@ export function WorkflowRunContentSkeleton() {
35
35
  return (
36
36
  <section
37
37
  aria-label="Loading workflow run content"
38
- className="min-h-160 rounded-8 border border-border-neutral-base bg-background-components-base p-16"
38
+ className="min-h-160 rounded-8 border border-border-neutral-base bg-background-components-base p-panel-compact"
39
39
  >
40
40
  <Skeleton className="h-160 w-full rounded-6" />
41
41
  </section>
@@ -59,9 +59,9 @@ export function WorkflowRunNotFound() {
59
59
  */
60
60
  export function WorkflowRunStaleError({query}: {query: QueryLoadErrorQuery}) {
61
61
  return (
62
- <div className="border-b border-border-neutral-base p-8">
62
+ <div className="border-b border-border-neutral-base p-tight">
63
63
  <Callout role="alert" type="error">
64
- <div className="flex items-center justify-between gap-8">
64
+ <div className="flex items-center justify-between gap-inline">
65
65
  <Text size="xs">Could not refresh this run.</Text>
66
66
  <Button
67
67
  type="button"
@@ -14,7 +14,12 @@ import {Text} from '@shipfox/react-ui/typography';
14
14
  import {useNavigate} from '@tanstack/react-router';
15
15
  import {type ReactNode, useEffect, useMemo} from 'react';
16
16
  import {buildRunAnnotationList, type RunAnnotationSummary} from '#core/run-annotation.js';
17
- import {isWorkflowRunTerminal, type Job, type WorkflowRunRerunMode} from '#core/workflow-run.js';
17
+ import {
18
+ type EvaluationTraceEntry,
19
+ isWorkflowRunTerminal,
20
+ type Job,
21
+ type WorkflowRunRerunMode,
22
+ } from '#core/workflow-run.js';
18
23
  import {withoutWorkflowRunSelectionSearch} from '#core/workflow-run-url-state.js';
19
24
  import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
20
25
  import {
@@ -32,7 +37,11 @@ import {
32
37
  import {JobGraph} from '../job-graph/index.js';
33
38
  import type {JobGraphSelectionSource} from '../job-graph/types.js';
34
39
  import {WorkflowRunSummary} from '../workflow-run-summary/index.js';
35
- import {RunAnnotationList, RunAnnotationSummaryLine} from '../workflow-run-tabs/index.js';
40
+ import {
41
+ type DerivedRunAnnotation,
42
+ RunAnnotationList,
43
+ RunAnnotationSummaryLine,
44
+ } from '../workflow-run-tabs/index.js';
36
45
  import {WorkflowSourceContent} from '../workflow-source-panel/index.js';
37
46
  import {RunWorkspaceNav} from './run-workspace-nav.js';
38
47
  import {resolveWorkflowRunSelection} from './workflow-run-selection.js';
@@ -339,7 +348,7 @@ function RunViewContent({
339
348
  highlightedLineRange={highlightedLineRange}
340
349
  />
341
350
  ) : (
342
- <div className="min-h-0 flex-1 overflow-auto p-24">{loadingOrError}</div>
351
+ <div className="min-h-0 flex-1 overflow-auto p-panel">{loadingOrError}</div>
343
352
  )}
344
353
  </div>
345
354
  </div>
@@ -382,8 +391,11 @@ function RunSectionContent({
382
391
  }) {
383
392
  if (section === 'summary') {
384
393
  return (
385
- <section aria-label="All jobs summary" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
386
- <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
394
+ <section
395
+ aria-label="All jobs summary"
396
+ className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
397
+ >
398
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-group px-frame">
387
399
  <Text as="h2" className="sr-only">
388
400
  All jobs summary
389
401
  </Text>
@@ -415,8 +427,11 @@ function RunSectionContent({
415
427
  }
416
428
 
417
429
  return (
418
- <section aria-label="Workflow source" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
419
- <div className="mx-auto flex min-h-full w-full max-w-[1120px] flex-col px-24">
430
+ <section
431
+ aria-label="Workflow source"
432
+ className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
433
+ >
434
+ <div className="mx-auto flex min-h-full w-full max-w-[1120px] flex-col px-frame">
420
435
  <Text as="h2" className="sr-only">
421
436
  Workflow source
422
437
  </Text>
@@ -480,14 +495,42 @@ function RunAnnotationsSection({
480
495
  : undefined,
481
496
  [records, run.jobs, selectedJob?.id, severity],
482
497
  );
498
+ const derivedAnnotations = useMemo<readonly DerivedRunAnnotation[] | undefined>(() => {
499
+ if (!records) return undefined;
500
+ const jobsWithAnnotations = new Set(records.map((annotation) => annotation.jobId));
501
+
502
+ return run.jobs
503
+ .filter((job) => {
504
+ const style = job.status === 'failed' ? 'error' : 'warning';
505
+ return (
506
+ (!selectedJob || selectedJob.id === job.id) &&
507
+ !jobsWithAnnotations.has(job.id) &&
508
+ (job.status === 'failed' || job.status === 'skipped') &&
509
+ job.jobExecutions.length === 0 &&
510
+ matchesDerivedAnnotationFilters(style, selection)
511
+ );
512
+ })
513
+ .map((job) => ({
514
+ id: `derived-${job.id}`,
515
+ style: job.status === 'failed' ? 'error' : 'warning',
516
+ body: derivedJobAnnotation(job),
517
+ }));
518
+ }, [records, run.jobs, selectedJob, selection]);
519
+ const hasSynthesizedJobAnnotations = run.jobs.some(
520
+ (job) =>
521
+ (job.status === 'failed' || job.status === 'skipped') && job.jobExecutions.length === 0,
522
+ );
483
523
 
484
524
  return (
485
- <section aria-label="Run annotations" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
486
- <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
525
+ <section
526
+ aria-label="Run annotations"
527
+ className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
528
+ >
529
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-group px-frame">
487
530
  <Text as="h2" className="sr-only">
488
531
  Annotations
489
532
  </Text>
490
- <div className="flex flex-wrap items-center justify-between gap-8">
533
+ <div className="flex flex-wrap items-center justify-between gap-inline">
491
534
  <RunAnnotationSummaryLine
492
535
  summary={annotationSummary}
493
536
  workspaceSlug={workspaceSlug}
@@ -507,12 +550,16 @@ function RunAnnotationsSection({
507
550
  key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
508
551
  query={annotations.query}
509
552
  entries={entries}
553
+ derivedAnnotations={derivedAnnotations}
510
554
  workspaceSlug={workspaceSlug}
511
555
  projectSlug={projectSlug}
512
556
  workflowRunId={run.id}
513
557
  runAttempt={run.runAttempt.attempt}
514
558
  // A run with no annotations at all offers no filter to clear, whatever the URL says.
515
- filtered={Boolean((severity || selectedJob) && (annotationSummary?.total ?? 0) > 0)}
559
+ filtered={Boolean(
560
+ (severity || selectedJob || selection?.annotation) &&
561
+ ((annotationSummary?.total ?? 0) > 0 || hasSynthesizedJobAnnotations),
562
+ )}
516
563
  filteredJobName={selectedJob?.displayName}
517
564
  filteredSeverity={severity}
518
565
  onClearFilters={onClearAnnotationFilters}
@@ -523,6 +570,59 @@ function RunAnnotationsSection({
523
570
  );
524
571
  }
525
572
 
573
+ function matchesDerivedAnnotationFilters(
574
+ style: 'warning' | 'error',
575
+ selection: WorkflowRunsSearch | undefined,
576
+ ): boolean {
577
+ if (selection?.severity && style !== selection.severity) return false;
578
+ // A selected annotation is a deep-link to a real annotation record. Synthetic diagnostics
579
+ // have no id or context, so they must not remain visible behind that selection.
580
+ return !selection?.annotation;
581
+ }
582
+
583
+ function derivedJobAnnotation(job: Job): string {
584
+ const reason = job.statusReason ? `Reason: \`${job.statusReason}\`` : null;
585
+ const traceSummary = formatConditionEvaluation(job.evaluationTrace);
586
+ const details = [reason, traceSummary].filter(Boolean).join('\n');
587
+
588
+ if (job.status === 'skipped') {
589
+ return [
590
+ `**${job.displayName}** was skipped before an execution was created.`,
591
+ '',
592
+ 'Review its dependencies or condition before re-running.',
593
+ details,
594
+ ]
595
+ .filter(Boolean)
596
+ .join('\n');
597
+ }
598
+ return [
599
+ `**${job.displayName}** failed before an execution was created.`,
600
+ '',
601
+ 'Check runner availability and workflow configuration before re-running.',
602
+ details,
603
+ ]
604
+ .filter(Boolean)
605
+ .join('\n');
606
+ }
607
+
608
+ function formatConditionEvaluation(
609
+ trace: readonly EvaluationTraceEntry[] | null | undefined,
610
+ ): string | null {
611
+ if (!trace?.length) return null;
612
+
613
+ return [
614
+ 'Condition evaluation:',
615
+ ...trace.map((entry) => {
616
+ if ('dropped' in entry) {
617
+ return `- ${entry.dropped} additional evaluation${entry.dropped === 1 ? '' : 's'} not recorded`;
618
+ }
619
+ const value =
620
+ entry.value === undefined || entry.value === '' ? '(empty)' : `\`${entry.value}\``;
621
+ return `- \`${entry.field}\` evaluated \`${entry.expression}\` to ${value}${entry.degraded ? ' (degraded)' : ''}`;
622
+ }),
623
+ ].join('\n');
624
+ }
625
+
526
626
  const ALL_ANNOTATION_JOBS = 'all-jobs';
527
627
 
528
628
  function AnnotationJobFilter({
@@ -566,11 +666,11 @@ function RunWorkspaceNavSkeleton() {
566
666
  return (
567
667
  <aside
568
668
  aria-label="Loading run navigation"
569
- className="hidden w-240 shrink-0 border-r border-border-neutral-base bg-background-neutral-background p-12 min-[768px]:block"
669
+ className="hidden w-240 shrink-0 border-r border-border-neutral-base bg-background-neutral-background p-panel-compact min-[768px]:block"
570
670
  >
571
- <div className="flex flex-col gap-16">
671
+ <div className="flex flex-col gap-group">
572
672
  <div className="h-32 rounded-4 bg-background-components-subtle" />
573
- <div className="flex flex-col gap-6">
673
+ <div className="flex flex-col gap-inline">
574
674
  <div className="h-16 w-64 rounded-4 bg-background-components-subtle" />
575
675
  <div className="h-32 rounded-4 bg-background-components-subtle" />
576
676
  <div className="h-32 rounded-4 bg-background-components-subtle" />
@@ -1,5 +1,6 @@
1
1
  import {type Duration, intervalToDuration} from 'date-fns';
2
2
  import type {Step} from './step.js';
3
+ import type {EvaluationTraceEntry} from './step-attempt.js';
3
4
 
4
5
  export type JobExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
5
6
  export type JobExecutionDisplayStatus = JobExecutionStatus;
@@ -28,11 +29,15 @@ interface JobExecutionFields {
28
29
  name: string;
29
30
  status: JobExecutionStatus;
30
31
  statusReason: string | null;
32
+ statusReasonMessage: string | null;
33
+ runner: string[] | null;
34
+ outputs: Record<string, unknown> | null;
31
35
  triggerEvents: WorkflowExecutionEvent[];
32
36
  queuedAt: string | null;
33
37
  startedAt: string | null;
34
38
  finishedAt: string | null;
35
39
  timedOutAt: string | null;
40
+ evaluationTrace: EvaluationTraceEntry[] | null;
36
41
  createdAt: string;
37
42
  updatedAt: string;
38
43
  steps: Step[];
@@ -45,11 +50,15 @@ export class JobExecution {
45
50
  name!: string;
46
51
  status!: JobExecutionStatus;
47
52
  statusReason!: string | null;
53
+ statusReasonMessage!: string | null;
54
+ runner!: string[] | null;
55
+ outputs!: Record<string, unknown> | null;
48
56
  triggerEvents!: WorkflowExecutionEvent[];
49
57
  queuedAt!: string | null;
50
58
  startedAt!: string | null;
51
59
  finishedAt!: string | null;
52
60
  timedOutAt!: string | null;
61
+ evaluationTrace!: EvaluationTraceEntry[] | null;
53
62
  createdAt!: string;
54
63
  updatedAt!: string;
55
64
  steps!: Step[];
@@ -3,6 +3,7 @@ import {
3
3
  type JobExecution,
4
4
  type JobExecutionDisplayDuration,
5
5
  } from './job-execution.js';
6
+ import type {EvaluationTraceEntry} from './step-attempt.js';
6
7
 
7
8
  export type JobStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'skipped';
8
9
  export type JobDisplayStatus = JobStatus | 'listening';
@@ -19,8 +20,10 @@ export type JobStatusReason =
19
20
  | 'run_cancelled'
20
21
  | 'timed_out'
21
22
  | 'runner_lost'
23
+ | 'output_too_large'
22
24
  | 'step_failed'
23
- | 'unknown';
25
+ | 'unknown'
26
+ | 'output_invalid';
24
27
  export interface JobListening {
25
28
  on: Array<{
26
29
  source: string;
@@ -75,6 +78,10 @@ interface JobFields {
75
78
  status: JobStatus;
76
79
  statusReason: JobStatusReason | null;
77
80
  carriedOver: boolean;
81
+ outputs: Record<string, unknown> | null;
82
+ success: string | null;
83
+ runner: string[] | null;
84
+ evaluationTrace: EvaluationTraceEntry[] | null;
78
85
  listening: JobListening | null;
79
86
  listenerStatus: ListenerStatus;
80
87
  resolutionReason: ResolutionReason | null;
@@ -94,6 +101,10 @@ export class Job {
94
101
  status!: JobStatus;
95
102
  statusReason!: JobStatusReason | null;
96
103
  carriedOver!: boolean;
104
+ outputs!: Record<string, unknown> | null;
105
+ success!: string | null;
106
+ runner!: string[] | null;
107
+ evaluationTrace!: EvaluationTraceEntry[] | null;
97
108
  listening!: JobListening | null;
98
109
  listenerStatus!: ListenerStatus;
99
110
  resolutionReason!: ResolutionReason | null;
@@ -13,6 +13,27 @@ export type StepAttemptDisplayDuration =
13
13
  | {state: 'fixed'; elapsed: Duration}
14
14
  | {state: 'live'; fromIso: string};
15
15
 
16
+ export interface EvaluationTraceValueEntry {
17
+ expression: string;
18
+ roots: string[];
19
+ fillTarget: string;
20
+ evaluatedAt: string;
21
+ field: string;
22
+ value?: string;
23
+ truncated?: boolean;
24
+ exprTruncated?: boolean;
25
+ reference?: boolean;
26
+ degraded?: boolean;
27
+ envKey?: string;
28
+ }
29
+
30
+ export interface EvaluationTraceLimitEntry {
31
+ truncated: true;
32
+ dropped: number;
33
+ }
34
+
35
+ export type EvaluationTraceEntry = EvaluationTraceValueEntry | EvaluationTraceLimitEntry;
36
+
16
37
  interface StepAttemptFields {
17
38
  id: string;
18
39
  stepId: string;
@@ -22,6 +43,8 @@ interface StepAttemptFields {
22
43
  status: string;
23
44
  exitCode: number | null;
24
45
  output: Record<string, unknown> | null;
46
+ outputs: Record<string, unknown> | null;
47
+ response: string | null;
25
48
  error: Record<string, unknown> | null;
26
49
  gateResult: StepGateResult;
27
50
  restartFeedback: string | null;
@@ -38,6 +61,8 @@ export class StepAttempt {
38
61
  status!: string;
39
62
  exitCode!: number | null;
40
63
  output!: Record<string, unknown> | null;
64
+ outputs!: Record<string, unknown> | null;
65
+ response!: string | null;
41
66
  error!: Record<string, unknown> | null;
42
67
  gateResult!: StepGateResult;
43
68
  restartFeedback!: string | null;
@@ -53,6 +78,14 @@ export class StepAttempt {
53
78
  }
54
79
  }
55
80
 
81
+ export interface StepAttemptDetail {
82
+ stepId: string;
83
+ attempt: number;
84
+ authoredConfig: Record<string, unknown> | null;
85
+ config: Record<string, unknown> | null;
86
+ evaluationTrace: EvaluationTraceEntry[] | null;
87
+ }
88
+
56
89
  export function stepAttemptDisplayDurationFromTimestamps({
57
90
  startedAt,
58
91
  finishedAt,
@@ -1,4 +1,4 @@
1
- import type {StepAttempt} from './step-attempt.js';
1
+ import type {EvaluationTraceEntry, StepAttempt} from './step-attempt.js';
2
2
 
3
3
  export type StepErrorReason =
4
4
  | 'checkout_failed'
@@ -71,8 +71,10 @@ export interface Step {
71
71
  name: string;
72
72
  sourceLocation: StepSourceLocation | null;
73
73
  status: string;
74
+ statusReason: string | null;
74
75
  type: string;
75
76
  config: Record<string, unknown>;
77
+ evaluationTrace: EvaluationTraceEntry[] | null;
76
78
  agentConfig: AgentStepConfig | null;
77
79
  error: StepError | null;
78
80
  position: number;