@shipfox/client-workflows 15.0.0 → 17.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 (225) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +41 -0
  5. package/dist/components/identifier/identifier.js +2 -2
  6. package/dist/components/identifier/identifier.js.map +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  8. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  9. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  10. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  11. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  12. package/dist/components/job-detail/job-context-panel.js +224 -0
  13. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  14. package/dist/components/job-detail/job-detail-header.d.ts +10 -1
  15. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  16. package/dist/components/job-detail/job-detail-header.js +58 -39
  17. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  19. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  20. package/dist/components/job-detail/job-detail-view.js +92 -41
  21. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  23. package/dist/components/job-detail/job-empty-states.js +19 -10
  24. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  25. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  26. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  27. package/dist/components/job-detail/json-code.d.ts +14 -0
  28. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  29. package/dist/components/job-detail/json-code.js +93 -0
  30. package/dist/components/job-detail/json-code.js.map +1 -0
  31. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  32. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  33. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  34. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  35. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  36. package/dist/components/job-detail/step-troubleshooting.js +493 -0
  37. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  39. package/dist/components/job-graph/job-graph-content.js +5 -4
  40. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  41. package/dist/components/job-graph/job-node.js +3 -3
  42. package/dist/components/job-graph/job-node.js.map +1 -1
  43. package/dist/components/step-list/index.d.ts +1 -0
  44. package/dist/components/step-list/index.d.ts.map +1 -1
  45. package/dist/components/step-list/index.js.map +1 -1
  46. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  47. package/dist/components/step-list/step-list-model.js +5 -0
  48. package/dist/components/step-list/step-list-model.js.map +1 -1
  49. package/dist/components/step-list/step-list.d.ts +5 -1
  50. package/dist/components/step-list/step-list.d.ts.map +1 -1
  51. package/dist/components/step-list/step-list.js +117 -82
  52. package/dist/components/step-list/step-list.js.map +1 -1
  53. package/dist/components/workflow-run-duration-label.js +1 -1
  54. package/dist/components/workflow-run-duration-label.js.map +1 -1
  55. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  56. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  60. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  62. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  66. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js +9 -13
  72. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts +3 -0
  74. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  75. package/dist/components/workflow-run-tabs/index.js +3 -0
  76. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  78. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  79. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  80. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  81. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  82. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  83. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  84. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  85. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +39 -0
  86. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-list.js +211 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  91. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +90 -25
  92. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  93. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  94. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  95. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  96. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  97. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
  98. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  99. package/dist/components/workflow-run-view/run-workspace-nav.js +16 -10
  100. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  101. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  102. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  103. package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
  104. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  105. package/dist/components/workflow-run-view/workflow-run-view.js +217 -59
  106. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  107. package/dist/core/entities/job-execution.d.ts +7 -0
  108. package/dist/core/entities/job-execution.d.ts.map +1 -1
  109. package/dist/core/entities/job-execution.js.map +1 -1
  110. package/dist/core/entities/job.d.ts +9 -0
  111. package/dist/core/entities/job.d.ts.map +1 -1
  112. package/dist/core/entities/job.js.map +1 -1
  113. package/dist/core/entities/step-attempt.d.ts +29 -0
  114. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  115. package/dist/core/entities/step-attempt.js.map +1 -1
  116. package/dist/core/entities/step.d.ts +3 -1
  117. package/dist/core/entities/step.d.ts.map +1 -1
  118. package/dist/core/entities/step.js.map +1 -1
  119. package/dist/core/run-annotation.d.ts +79 -0
  120. package/dist/core/run-annotation.d.ts.map +1 -0
  121. package/dist/core/run-annotation.js +125 -0
  122. package/dist/core/run-annotation.js.map +1 -0
  123. package/dist/core/workflow-run.d.ts +1 -1
  124. package/dist/core/workflow-run.d.ts.map +1 -1
  125. package/dist/core/workflow-run.js.map +1 -1
  126. package/dist/hooks/api/annotations.d.ts +63 -0
  127. package/dist/hooks/api/annotations.d.ts.map +1 -0
  128. package/dist/hooks/api/annotations.js +145 -0
  129. package/dist/hooks/api/annotations.js.map +1 -0
  130. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  131. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  132. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  133. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  134. package/dist/hooks/api/run-annotations.d.ts +25 -0
  135. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  136. package/dist/hooks/api/run-annotations.js +78 -0
  137. package/dist/hooks/api/run-annotations.js.map +1 -0
  138. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  139. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  140. package/dist/hooks/api/step-attempt-detail.js +43 -0
  141. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  142. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  143. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  144. package/dist/hooks/api/workflow-run-mapper.js +47 -0
  145. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  146. package/dist/pages/project-workflows-page.js +19 -19
  147. package/dist/pages/project-workflows-page.js.map +1 -1
  148. package/dist/pages/workflow-run-list-page.js +1 -1
  149. package/dist/pages/workflow-run-list-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +7 -2
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +5 -6
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/tsconfig.test.tsbuildinfo +1 -1
  155. package/package.json +10 -9
  156. package/src/components/identifier/identifier.tsx +2 -2
  157. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  158. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  159. package/src/components/job-detail/job-context-panel.tsx +237 -0
  160. package/src/components/job-detail/job-detail-header.test.tsx +3 -0
  161. package/src/components/job-detail/job-detail-header.tsx +65 -34
  162. package/src/components/job-detail/job-detail-view.stories.tsx +555 -48
  163. package/src/components/job-detail/job-detail-view.tsx +101 -42
  164. package/src/components/job-detail/job-empty-states.tsx +23 -10
  165. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  166. package/src/components/job-detail/json-code.tsx +120 -0
  167. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  168. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  169. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  170. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  171. package/src/components/job-detail/step-troubleshooting.tsx +537 -0
  172. package/src/components/job-graph/job-graph-content.tsx +4 -3
  173. package/src/components/job-graph/job-node.tsx +3 -3
  174. package/src/components/step-list/index.ts +1 -0
  175. package/src/components/step-list/step-list-model.ts +5 -0
  176. package/src/components/step-list/step-list.test.tsx +75 -2
  177. package/src/components/step-list/step-list.tsx +73 -20
  178. package/src/components/workflow-run-duration-label.tsx +1 -1
  179. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  180. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  181. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  182. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  183. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  184. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  185. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  186. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  187. package/src/components/workflow-run-summary/workflow-run-summary.tsx +12 -12
  188. package/src/components/workflow-run-tabs/index.ts +7 -0
  189. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  190. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  191. package/src/components/workflow-run-tabs/run-annotation-list.tsx +273 -0
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  193. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +95 -20
  194. package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
  195. package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
  196. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +44 -0
  197. package/src/components/workflow-run-view/run-workspace-nav.tsx +22 -9
  198. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  199. package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
  200. package/src/components/workflow-run-view/workflow-run-view.tsx +255 -37
  201. package/src/core/entities/job-execution.ts +7 -0
  202. package/src/core/entities/job.ts +9 -0
  203. package/src/core/entities/step-attempt.ts +33 -0
  204. package/src/core/entities/step.ts +3 -1
  205. package/src/core/run-annotation.test.ts +315 -0
  206. package/src/core/run-annotation.ts +244 -0
  207. package/src/core/workflow-run.ts +4 -0
  208. package/src/hooks/api/annotations.test.ts +84 -0
  209. package/src/hooks/api/annotations.ts +240 -0
  210. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  211. package/src/hooks/api/run-annotations.ts +137 -0
  212. package/src/hooks/api/step-attempt-detail.ts +67 -0
  213. package/src/hooks/api/workflow-run-mapper.ts +46 -0
  214. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  215. package/src/pages/project-workflows-page.tsx +19 -19
  216. package/src/pages/workflow-job-detail-page.test.tsx +118 -1
  217. package/src/pages/workflow-run-list-page.tsx +1 -1
  218. package/src/routes/inputs.ts +7 -2
  219. package/test/fixtures/workflow-run.ts +7 -0
  220. package/tsconfig.build.tsbuildinfo +1 -1
  221. package/dist/core/workflow-run-tabs.d.ts +0 -9
  222. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  223. package/dist/core/workflow-run-tabs.js +0 -3
  224. package/dist/core/workflow-run-tabs.js.map +0 -1
  225. package/src/core/workflow-run-tabs.ts +0 -8
@@ -9,8 +9,12 @@ 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';
13
- import type {Job, JobExecution} from '#core/workflow-run.js';
12
+ import {type RefObject, useEffect, useRef, useState} from 'react';
13
+ import type {RunAnnotationSummary} from '#core/run-annotation.js';
14
+ import {summarizeJobAnnotations} from '#core/run-annotation.js';
15
+ import {isWorkflowRunTerminal, type Job, type JobExecution} from '#core/workflow-run.js';
16
+ import {useWorkflowRunAnnotationSummaryQuery} from '#hooks/api/annotations.js';
17
+ import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
14
18
  import type {useWorkflowRunAttemptQuery} from '#hooks/api/workflow-runs.js';
15
19
  import {
16
20
  type WorkflowJobSearch,
@@ -22,15 +26,13 @@ import {
22
26
  WorkflowRunNotFound,
23
27
  WorkflowRunStaleError,
24
28
  } from '../workflow-run-view/workflow-run-states.js';
25
- import {AgentConfigFailureCallout} from './agent-config-failure-callout.js';
29
+ import {JobContextPanel} from './job-context-panel.js';
26
30
  import {JobDetailHeader} from './job-detail-header.js';
27
31
  import {
28
32
  CarriedOverStepPanel,
29
33
  emptyStateForJob,
30
34
  emptyStateForMissingExecution,
31
- isAgentConfigFailure,
32
35
  jobSucceededSummary,
33
- toSelectedAttemptError,
34
36
  } from './job-empty-states.js';
35
37
  import {
36
38
  resolveWorkflowJobSelection,
@@ -38,6 +40,9 @@ import {
38
40
  workflowJobLandingSelection,
39
41
  } from './job-selection.js';
40
42
  import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
43
+ import {StepInspectorSheet} from './step-troubleshooting.js';
44
+
45
+ type InspectorState = {key: string; attemptId: string | null};
41
46
 
42
47
  export interface JobDetailViewProps {
43
48
  workspaceSlug: string;
@@ -66,6 +71,37 @@ export function JobDetailView({
66
71
  const pageScrollRef = useRef<HTMLDivElement>(null);
67
72
  const landingSelectionRef = useRef<FrozenLandingSelection | undefined>(undefined);
68
73
  const hasLoadedData = query.data !== undefined;
74
+ // Reuse the run workspace's bounded annotation read for the job header chip. The separate
75
+ // summary query below stays counts-only and is scoped to the inspector's selected execution.
76
+ const annotations = useRunAnnotationsQuery({
77
+ workflowRunId,
78
+ runAttempt: query.data?.runAttempt.attempt,
79
+ });
80
+ const jobAnnotationSummary = annotations.annotations
81
+ ? summarizeJobAnnotations(annotations.annotations, jobId, {
82
+ truncated: annotations.summary?.truncated ?? false,
83
+ })
84
+ : undefined;
85
+ const inspectorResetKey = `${jobId}:${search.jobExecutionId ?? ''}`;
86
+ const [inspectorState, setInspectorState] = useState<InspectorState>(() => ({
87
+ key: inspectorResetKey,
88
+ attemptId: null,
89
+ }));
90
+ const inspectorOpenAttemptId =
91
+ inspectorState.key === inspectorResetKey ? inspectorState.attemptId : null;
92
+ const annotationJob = query.data?.jobs.find((candidate) => candidate.id === jobId);
93
+ const annotationExecutionId = annotationJob
94
+ ? resolveWorkflowJobSelection({job: annotationJob, selection: search}).jobExecution?.id
95
+ : search.jobExecutionId;
96
+ const annotationPolling = query.data
97
+ ? !isWorkflowRunTerminal(query.data.runAttempt.status)
98
+ : true;
99
+ const annotationSummaryQuery = useWorkflowRunAnnotationSummaryQuery(
100
+ query.data?.id,
101
+ query.data?.runAttempt.attempt,
102
+ annotationExecutionId,
103
+ {polling: annotationPolling},
104
+ );
69
105
 
70
106
  useEffect(() => {
71
107
  if (!jobId || !hasLoadedData) return;
@@ -173,6 +209,10 @@ export function JobDetailView({
173
209
  });
174
210
  }
175
211
 
212
+ function onInspectorOpenChange(attemptId: string | null) {
213
+ setInspectorState({key: inspectorResetKey, attemptId});
214
+ }
215
+
176
216
  function retargetToRunningStep() {
177
217
  if (!runningSelection || !selectedJobExecution) return;
178
218
  onSelectionChange({
@@ -196,16 +236,23 @@ export function JobDetailView({
196
236
  attempt={newerAttempt}
197
237
  />
198
238
  ) : null}
199
- <div ref={pageScrollRef} className="@container min-h-0 flex-1 overflow-auto pb-24">
200
- <section
201
- aria-label={`${job.displayName} logs`}
202
- className="mx-auto flex w-full max-w-[1120px] flex-col gap-8 px-24"
203
- >
239
+ <div ref={pageScrollRef} className="@container min-h-0 flex-1 overflow-auto pb-panel">
240
+ <section aria-label={`${job.displayName} logs`} className="flex w-full flex-col">
204
241
  <section className="min-w-0 overflow-hidden">
205
242
  <JobDetailHeader
206
243
  job={job}
207
244
  selectedJobExecution={selectedJobExecution}
208
245
  onSelectedJobExecutionChange={selectExecution}
246
+ workspaceSlug={workspaceSlug}
247
+ projectSlug={projectSlug}
248
+ workflowRunId={run.id}
249
+ runAttempt={run.runAttempt.attempt}
250
+ annotationSummary={jobAnnotationSummary}
251
+ jobContext={
252
+ selectedJobExecution ? (
253
+ <JobContextPanel job={job} execution={selectedJobExecution} />
254
+ ) : undefined
255
+ }
209
256
  />
210
257
  <Text as="h2" className="sr-only">
211
258
  Logs
@@ -217,15 +264,30 @@ export function JobDetailView({
217
264
  selectedAttemptId={selectedAttemptId}
218
265
  defaultSelectedAttemptId={landingSelection?.attemptId}
219
266
  onSelectedAttemptChange={selectAttempt}
267
+ inspectorOpenAttemptId={inspectorOpenAttemptId}
268
+ onInspectorOpenChange={onInspectorOpenChange}
220
269
  autoSelectActiveAttempt
221
270
  emptyState={emptyStateForJob(job, selectedJobExecution)}
222
271
  showHeader={false}
223
- className="rounded-none border-0"
272
+ className="rounded-none border-0 bg-transparent"
224
273
  renderExpandedStep={(context) => (
225
- <ExpandedStep
226
- context={context}
274
+ <ExpandedStep context={context} pageScrollRef={pageScrollRef} />
275
+ )}
276
+ renderInspector={(entry) => (
277
+ <StepInspectorSheet
278
+ entry={entry}
279
+ open
280
+ onOpenChange={(open) => onInspectorOpenChange(open ? entry.id : null)}
227
281
  workspaceSlug={workspaceSlug}
228
- pageScrollRef={pageScrollRef}
282
+ projectSlug={projectSlug}
283
+ workflowRunId={run.id}
284
+ runAttempt={run.runAttempt.attempt}
285
+ jobId={job.id}
286
+ annotationCount={annotationCountForStep(
287
+ annotationSummaryQuery.data,
288
+ entry.step.id,
289
+ entry.attempt,
290
+ )}
229
291
  />
230
292
  )}
231
293
  />
@@ -237,7 +299,7 @@ export function JobDetailView({
237
299
  <div
238
300
  role="status"
239
301
  aria-live="polite"
240
- 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"
302
+ className="flex min-w-0 items-center justify-between gap-inline border-t border-border-neutral-base px-row py-row"
241
303
  >
242
304
  <Text size="xs" className="min-w-0 text-foreground-neutral-muted">
243
305
  Run moved on to{' '}
@@ -248,7 +310,7 @@ export function JobDetailView({
248
310
  </Text>
249
311
  <button
250
312
  type="button"
251
- 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"
313
+ 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"
252
314
  onClick={retargetToRunningStep}
253
315
  >
254
316
  Jump to it
@@ -256,7 +318,7 @@ export function JobDetailView({
256
318
  </div>
257
319
  ) : null}
258
320
  {succeededSummary ? (
259
- <Text size="xs" className="px-8 text-foreground-neutral-muted">
321
+ <Text size="xs" className="px-row py-row text-foreground-neutral-muted">
260
322
  {succeededSummary}
261
323
  </Text>
262
324
  ) : null}
@@ -269,39 +331,27 @@ export function JobDetailView({
269
331
 
270
332
  function ExpandedStep({
271
333
  context,
272
- workspaceSlug,
273
334
  pageScrollRef,
274
335
  }: {
275
336
  context: StepExpandedContext;
276
- workspaceSlug: string;
277
337
  pageScrollRef: RefObject<HTMLDivElement | null>;
278
338
  }) {
279
339
  if (context.carriedOver) return <CarriedOverStepPanel />;
280
340
 
281
- const selectedAttemptError =
282
- toSelectedAttemptError(context.step, context.attemptError) ?? context.step.error;
283
-
284
341
  return (
285
342
  <section
286
343
  role="region"
287
344
  tabIndex={0}
288
345
  aria-label={`${context.stepLabel} output, attempt ${context.attempt}`}
289
- 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"
346
+ 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"
290
347
  >
291
- {isAgentConfigFailure(context.step, selectedAttemptError) ? (
292
- <AgentConfigFailureCallout
293
- workspaceSlug={workspaceSlug}
294
- config={context.step.agentConfig}
295
- error={selectedAttemptError}
296
- />
297
- ) : null}
298
348
  <StepAttemptLogPanel
299
349
  stepId={context.stepId}
300
350
  attempt={context.attempt}
301
351
  attemptStatus={context.attemptStatus}
302
352
  attemptStartedAt={context.attemptStartedAt}
303
353
  pageScrollRef={pageScrollRef}
304
- surfaceClassName="rounded-8"
354
+ surfaceClassName="rounded-none border-0 bg-transparent shadow-none"
305
355
  />
306
356
  </section>
307
357
  );
@@ -311,7 +361,7 @@ function EmptyStateForMissingExecution({job}: {job: Job}) {
311
361
  const emptyState = emptyStateForMissingExecution(job);
312
362
  return (
313
363
  <EmptyState
314
- className="min-h-120 px-16 py-20"
364
+ className="min-h-120 p-panel"
315
365
  icon="componentLine"
316
366
  title={emptyState.title}
317
367
  description={emptyState.description}
@@ -332,7 +382,7 @@ function JobNotFoundState({
332
382
  search: WorkflowJobSearch;
333
383
  }) {
334
384
  return (
335
- <div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-12 p-24">
385
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-cluster p-panel">
336
386
  <EmptyState
337
387
  icon="componentLine"
338
388
  title="Job not found"
@@ -347,7 +397,7 @@ function JobNotFoundState({
347
397
  {runAttempt: search.runAttempt},
348
398
  ) as never
349
399
  }
350
- 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"
400
+ 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"
351
401
  >
352
402
  <Icon name="arrowLeftLine" size={14} aria-hidden="true" />
353
403
  Back to run summary
@@ -370,8 +420,8 @@ function NewerAttemptNotice({
370
420
  attempt: number;
371
421
  }) {
372
422
  return (
373
- <div role="status" className="border-b border-border-neutral-base px-16 py-8">
374
- <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">
423
+ <div role="status" className="border-b border-border-neutral-base px-row py-row">
424
+ <div className="flex items-center justify-between gap-inline">
375
425
  <Text size="xs" className="text-foreground-neutral-muted">
376
426
  A newer run attempt is available.
377
427
  </Text>
@@ -379,7 +429,7 @@ function NewerAttemptNotice({
379
429
  to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId"
380
430
  params={{workspaceSlug, projectSlug, workflowRunId: runId, jobId}}
381
431
  search={workflowJobSearchParams({runAttempt: attempt}) as never}
382
- 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"
432
+ 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"
383
433
  >
384
434
  View attempt #{attempt}
385
435
  </Link>
@@ -414,21 +464,30 @@ function findAttempt(jobExecution: JobExecution, attemptId: string) {
414
464
  return undefined;
415
465
  }
416
466
 
467
+ function annotationCountForStep(
468
+ summary: RunAnnotationSummary | undefined,
469
+ stepId: string,
470
+ attempt: number,
471
+ ): number | undefined {
472
+ return summary?.stepCounts?.find((entry) => entry.stepId === stepId && entry.attempt === attempt)
473
+ ?.total;
474
+ }
475
+
417
476
  function JobDetailSkeleton() {
418
477
  return (
419
- <div className="min-h-0 min-w-0 flex-1 overflow-auto px-24 pb-24">
420
- <div className="mx-auto max-w-[1120px] overflow-hidden">
421
- <header className="flex items-center gap-12 border-b border-border-neutral-base px-16 py-12">
478
+ <div className="min-h-0 min-w-0 flex-1 overflow-auto pb-panel">
479
+ <div className="w-full">
480
+ <header className="flex items-center gap-cluster border-b border-border-neutral-base px-row py-row">
422
481
  <Skeleton className="size-20 rounded-full" />
423
482
  <Skeleton className="h-20 w-160 rounded-4" />
424
483
  <Skeleton className="h-24 w-72 rounded-6" />
425
484
  <Skeleton className="h-24 w-180 rounded-4" />
426
485
  </header>
427
- <div className="p-16">
486
+ <div className="p-panel-compact">
428
487
  {JOB_DETAIL_SKELETON_ROWS.map((row) => (
429
488
  <div
430
489
  key={row}
431
- className="flex min-h-44 items-center gap-8 border-b border-border-neutral-base last:border-b-0"
490
+ className="flex min-h-44 items-center gap-inline border-b border-border-neutral-base last:border-b-0"
432
491
  >
433
492
  <Skeleton className="size-14 rounded-full" />
434
493
  <Skeleton className="h-16 w-180 rounded-4" />
@@ -25,11 +25,14 @@ export function emptyStateForJob(
25
25
  job.mode === 'listening'
26
26
  ? deriveJobExecutionDisplayStatus(jobExecution)
27
27
  : deriveJobDisplayStatus(job);
28
+ const runner = jobExecution.runner?.length ? jobExecution.runner : job.runner;
28
29
 
29
30
  if (jobExecution.status === 'running' && displayStatus === 'pending') {
30
31
  return {
31
- title: 'Runner preparing job',
32
- description: 'A runner is assigned. Steps will appear here when work begins.',
32
+ title: runner?.length ? 'Runner preparing job' : 'Waiting for a runner',
33
+ description: runner?.length
34
+ ? `Runner ${runner.join(', ')} is preparing the job. Steps will appear here when work begins.`
35
+ : 'No runner has claimed this job yet. Steps will appear here when a runner starts work.',
33
36
  status: displayStatus,
34
37
  };
35
38
  }
@@ -61,7 +64,7 @@ export function emptyStateForJob(
61
64
  if (displayStatus === 'failed') {
62
65
  return {
63
66
  title: 'Job failed before its first step started',
64
- description: preStepFailureDescription(jobExecution.statusReason ?? job.statusReason),
67
+ description: preStepFailureDescription(jobExecution.statusReason ?? job.statusReason, runner),
65
68
  status: displayStatus,
66
69
  };
67
70
  }
@@ -126,6 +129,14 @@ export function emptyStateForMissingExecution(job: Job): StepListEmptyState {
126
129
  };
127
130
  }
128
131
 
132
+ if (job.status === 'failed') {
133
+ return {
134
+ title: 'Job failed before an execution was created',
135
+ description: preStepFailureDescription(job.statusReason, job.runner),
136
+ status: 'failed',
137
+ };
138
+ }
139
+
129
140
  return {
130
141
  title: 'Execution details unavailable',
131
142
  description: 'This job finished, but no job execution record is available.',
@@ -155,17 +166,19 @@ export function skippedJobDescription(reason: Job['statusReason']): string {
155
166
  }
156
167
  }
157
168
 
158
- function preStepFailureDescription(reason: string | null): string {
169
+ function preStepFailureDescription(reason: string | null, runner: string[] | null = null): string {
170
+ const runnerCopy = runner?.length ? ` Required runner labels: ${runner.join(', ')}.` : '';
171
+
159
172
  switch (reason) {
160
173
  case 'runner_lost':
161
- return 'The runner stopped responding before work began. Check runner availability before re-running the workflow.';
174
+ return `The runner stopped responding before work began.${runnerCopy} Check runner availability before re-running the workflow.`;
162
175
  case 'timed_out':
163
- return 'The job timed out before work began. Check runner capacity and timeout configuration before re-running the workflow.';
176
+ return `The job timed out before work began.${runnerCopy} Check runner capacity and timeout configuration before re-running the workflow.`;
164
177
  case 'user_cancelled':
165
178
  case 'run_cancelled':
166
179
  return 'The execution ended while the run was being cancelled.';
167
180
  case 'step_failed':
168
- return 'The execution failed before step details were recorded. Review run annotations before re-running the workflow.';
181
+ return `The execution failed before step details were recorded.${runnerCopy} Review run annotations before re-running the workflow.`;
169
182
  case 'condition_errored':
170
183
  return 'The job condition could not be evaluated. Review run annotations before re-running the workflow.';
171
184
  case 'dependency_not_completed':
@@ -176,9 +189,9 @@ function preStepFailureDescription(reason: string | null): string {
176
189
  return 'The job condition did not match, so no work started.';
177
190
  case 'unknown':
178
191
  case null:
179
- return 'Shipfox did not record a failure reason. Re-run only after checking the runner and workflow configuration.';
192
+ return `Shipfox did not record a failure reason.${runnerCopy} Re-run only after checking the runner and workflow configuration.`;
180
193
  default:
181
- return `The execution ended because ${humanizeFailureReason(reason)}. Resolve that condition before re-running the workflow.`;
194
+ return `The execution ended because ${humanizeFailureReason(reason)}.${runnerCopy} Resolve that condition before re-running the workflow.`;
182
195
  }
183
196
  }
184
197
 
@@ -189,7 +202,7 @@ function humanizeFailureReason(reason: string): string {
189
202
  export function CarriedOverStepPanel() {
190
203
  return (
191
204
  <EmptyState
192
- className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base px-16 py-20"
205
+ className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base p-panel"
193
206
  icon="componentLine"
194
207
  title="Carried over from a previous attempt"
195
208
  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}
@@ -0,0 +1,120 @@
1
+ import {
2
+ CodeBlock,
3
+ CodeBlockBody,
4
+ CodeBlockContent,
5
+ CodeBlockCopyButton,
6
+ CodeBlockFilename,
7
+ CodeBlockFiles,
8
+ CodeBlockHeader,
9
+ CodeBlockItem,
10
+ CodeTabs,
11
+ } from '@shipfox/react-ui/code-block';
12
+ import {Text} from '@shipfox/react-ui/typography';
13
+ import {useState} from 'react';
14
+
15
+ export interface JsonCodeEntry {
16
+ filename: string;
17
+ label?: string | undefined;
18
+ value: unknown;
19
+ }
20
+
21
+ export function JsonCode({
22
+ title,
23
+ value,
24
+ emptyMessage,
25
+ }: {
26
+ title?: string | undefined;
27
+ value: unknown;
28
+ emptyMessage?: string | undefined;
29
+ }) {
30
+ const filename = title ?? 'value.json';
31
+
32
+ return (
33
+ <div className="flex min-w-0 flex-col gap-tight">
34
+ {title ? (
35
+ <Text size="xs" className="text-foreground-neutral-muted">
36
+ {title}
37
+ </Text>
38
+ ) : null}
39
+ {emptyMessage && isEmptyObject(value) ? (
40
+ <Text size="xs" className="text-foreground-neutral-muted">
41
+ {emptyMessage}
42
+ </Text>
43
+ ) : (
44
+ <JsonCodeBlock entries={[{filename, value}]} />
45
+ )}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export function JsonCodeTabs({entries}: {entries: readonly JsonCodeEntry[]}) {
51
+ const entryKeys = entries.map((entry) => entry.label ?? entry.filename);
52
+ const [activeKey, setActiveKey] = useState(entryKeys[0] ?? '');
53
+ const activeValue = entryKeys.includes(activeKey) ? activeKey : (entryKeys[0] ?? '');
54
+ const codes = Object.fromEntries(
55
+ entries.map((entry) => [entry.label ?? entry.filename, stringifyJson(entry.value)]),
56
+ );
57
+
58
+ return (
59
+ <CodeTabs
60
+ value={activeValue}
61
+ onValueChange={setActiveKey}
62
+ codes={codes}
63
+ lang="json"
64
+ syntaxHighlighting={false}
65
+ className="h-auto min-h-0 rounded-8 bg-background-contrast-base shadow-button-neutral"
66
+ />
67
+ );
68
+ }
69
+
70
+ function JsonCodeBlock({entries}: {entries: readonly JsonCodeEntry[]}) {
71
+ const data = entries.map((entry) => ({
72
+ filename: entry.filename,
73
+ language: 'json',
74
+ code: stringifyJson(entry.value),
75
+ }));
76
+
77
+ return (
78
+ <CodeBlock
79
+ data={data}
80
+ className="h-auto min-h-0 rounded-6 bg-background-contrast-base shadow-button-neutral"
81
+ >
82
+ <CodeBlockHeader className="bg-background-contrast-base p-tight">
83
+ <CodeBlockFiles>
84
+ {(item) => {
85
+ const entry = entries.find((candidate) => candidate.filename === item.filename);
86
+ return (
87
+ <CodeBlockFilename value={item.filename}>
88
+ {entry?.label ?? item.filename}
89
+ </CodeBlockFilename>
90
+ );
91
+ }}
92
+ </CodeBlockFiles>
93
+ <CodeBlockCopyButton />
94
+ </CodeBlockHeader>
95
+ <CodeBlockBody>
96
+ {(item) => (
97
+ <CodeBlockItem value={item.filename}>
98
+ <CodeBlockContent language="json" syntaxHighlighting={false}>
99
+ {item.code}
100
+ </CodeBlockContent>
101
+ </CodeBlockItem>
102
+ )}
103
+ </CodeBlockBody>
104
+ </CodeBlock>
105
+ );
106
+ }
107
+
108
+ function isEmptyObject(value: unknown): boolean {
109
+ return (
110
+ typeof value === 'object' &&
111
+ value !== null &&
112
+ !Array.isArray(value) &&
113
+ Object.keys(value).length === 0
114
+ );
115
+ }
116
+
117
+ function stringifyJson(value: unknown): string {
118
+ const serialized = JSON.stringify(value, null, 2);
119
+ return serialized === undefined ? String(value) : serialized;
120
+ }
@@ -203,6 +203,7 @@ describe('StepAttemptLogPanel', () => {
203
203
  renderPanel({attemptStatus: 'succeeded'});
204
204
 
205
205
  expect(await screen.findByText('hello')).toBeInTheDocument();
206
+ expect(screen.getByRole('log')).toHaveAttribute('aria-live', 'off');
206
207
  });
207
208
 
208
209
  test('renders a terminal closed empty stream as no output', async () => {
@@ -100,10 +100,15 @@ export function StepAttemptLogPanel({
100
100
  }
101
101
 
102
102
  return (
103
- <div className="flex min-w-0 flex-col gap-8">
103
+ <div className="flex min-w-0 flex-col gap-inline">
104
104
  {staleError ? (
105
- <Callout role="alert" type="warning" className="px-10 py-8">
106
- <div className="flex min-w-0 flex-1 items-center justify-between gap-8">
105
+ <Callout
106
+ role="alert"
107
+ type="warning"
108
+ variant="secondary"
109
+ className="rounded-none border-b border-border-neutral-base px-0 py-row shadow-none"
110
+ >
111
+ <div className="flex min-w-0 flex-1 items-center justify-between gap-inline">
107
112
  <Text size="xs">Could not refresh logs.</Text>
108
113
  <Button
109
114
  type="button"
@@ -121,6 +126,7 @@ export function StepAttemptLogPanel({
121
126
  records={records}
122
127
  emptyState={query.data?.complete ? 'complete' : 'pending'}
123
128
  anchorToFailure={anchorToFailure}
129
+ ariaLive={attemptStatus === 'running' ? 'polite' : 'off'}
124
130
  className={surfaceClassName}
125
131
  />
126
132
  </div>
@@ -138,7 +144,7 @@ function StepLogsLoadingSurface({label, className}: {label: string; className: s
138
144
  function StepLogsWaitingSurface({startedAt, className}: {startedAt: string; className: string}) {
139
145
  return (
140
146
  <div role="status" aria-label="Waiting for logs" className={className}>
141
- <Text size="xs" className="px-10 py-12 text-foreground-neutral-muted">
147
+ <Text size="xs" className="px-tight py-row text-foreground-neutral-muted">
142
148
  Waiting for output ·{' '}
143
149
  <span className="font-code tabular-nums" aria-hidden="true">
144
150
  <JobExecutionTimeText time={{state: 'live', fromIso: startedAt}} />
@@ -174,8 +180,13 @@ function isTerminalAttemptStatus(status: string): boolean {
174
180
 
175
181
  function StepLogsError({retrying, onRetry}: {retrying: boolean; onRetry: () => void}) {
176
182
  return (
177
- <Callout role="alert" type="error" className="px-10 py-8">
178
- <div className="flex min-w-0 flex-1 items-center justify-between gap-8">
183
+ <Callout
184
+ role="alert"
185
+ type="error"
186
+ variant="secondary"
187
+ className="rounded-none border-b border-border-neutral-base px-0 py-row shadow-none"
188
+ >
189
+ <div className="flex min-w-0 flex-1 items-center justify-between gap-inline">
179
190
  <Text size="xs">Could not load logs.</Text>
180
191
  <Button type="button" size="2xs" variant="secondary" isLoading={retrying} onClick={onRetry}>
181
192
  Retry