@shipfox/client-workflows 14.0.1 → 16.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 (272) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.storybook/preview.tsx +7 -2
  3. package/.turbo/turbo-build.log +1 -1
  4. package/CHANGELOG.md +42 -0
  5. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
  6. package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
  7. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
  8. package/dist/components/job-detail/job-detail-header.d.ts +15 -0
  9. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
  10. package/dist/components/job-detail/job-detail-header.js +124 -0
  11. package/dist/components/job-detail/job-detail-header.js.map +1 -0
  12. package/dist/components/job-detail/job-detail-view.d.ts +16 -0
  13. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
  14. package/dist/components/job-detail/job-detail-view.js +420 -0
  15. package/dist/components/job-detail/job-detail-view.js.map +1 -0
  16. package/dist/components/job-detail/job-empty-states.d.ts +10 -0
  17. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
  18. package/dist/components/job-detail/job-empty-states.js +195 -0
  19. package/dist/components/job-detail/job-empty-states.js.map +1 -0
  20. package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
  21. package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
  22. package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
  23. package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
  24. package/dist/components/job-detail/job-selection.d.ts +24 -0
  25. package/dist/components/job-detail/job-selection.d.ts.map +1 -0
  26. package/dist/components/job-detail/job-selection.js +66 -0
  27. package/dist/components/job-detail/job-selection.js.map +1 -0
  28. package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
  29. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
  30. package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
  31. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
  32. package/dist/components/job-graph/job-graph-content.js +1 -1
  33. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  34. package/dist/components/step-list/step-list-model.js +2 -2
  35. package/dist/components/step-list/step-list-model.js.map +1 -1
  36. package/dist/components/step-list/step-list.d.ts +1 -0
  37. package/dist/components/step-list/step-list.d.ts.map +1 -1
  38. package/dist/components/step-list/step-list.js +29 -4
  39. package/dist/components/step-list/step-list.js.map +1 -1
  40. package/dist/components/workflow-run-duration-label.d.ts +9 -4
  41. package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
  42. package/dist/components/workflow-run-duration-label.js +27 -7
  43. package/dist/components/workflow-run-duration-label.js.map +1 -1
  44. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  45. package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
  46. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  47. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  48. package/dist/components/workflow-run-summary/workflow-run-summary.js +69 -64
  49. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  50. package/dist/components/workflow-run-tabs/index.d.ts +3 -3
  51. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  52. package/dist/components/workflow-run-tabs/index.js +3 -3
  53. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  54. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  55. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  56. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  57. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  58. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  59. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  60. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  61. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  62. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
  63. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  64. package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
  65. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  66. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  67. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  68. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
  69. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  70. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
  71. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
  72. package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
  73. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
  74. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  75. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  76. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  77. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  78. package/dist/components/workflow-run-view/index.d.ts +0 -1
  79. package/dist/components/workflow-run-view/index.d.ts.map +1 -1
  80. package/dist/components/workflow-run-view/index.js +0 -1
  81. package/dist/components/workflow-run-view/index.js.map +1 -1
  82. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
  83. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
  84. package/dist/components/workflow-run-view/run-workspace-nav.js +277 -0
  85. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -0
  86. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  87. package/dist/components/workflow-run-view/workflow-run-selection.js +1 -14
  88. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -10
  90. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  91. package/dist/components/workflow-run-view/workflow-run-view.js +351 -239
  92. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  93. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  94. package/dist/components/workflow-status/status-visuals.js +10 -2
  95. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  96. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
  97. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  98. package/dist/components/workflow-status/workflow-status-icon.js +13 -2
  99. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  100. package/dist/core/entities/job-execution.d.ts +9 -0
  101. package/dist/core/entities/job-execution.d.ts.map +1 -1
  102. package/dist/core/entities/job-execution.js +13 -3
  103. package/dist/core/entities/job-execution.js.map +1 -1
  104. package/dist/core/entities/job.d.ts.map +1 -1
  105. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  106. package/dist/core/entities/step.d.ts +2 -0
  107. package/dist/core/entities/step.d.ts.map +1 -1
  108. package/dist/core/entities/step.js +13 -0
  109. package/dist/core/entities/step.js.map +1 -1
  110. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  111. package/dist/core/entities/workflow-run.d.ts +56 -1
  112. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  113. package/dist/core/entities/workflow-run.js +98 -0
  114. package/dist/core/entities/workflow-run.js.map +1 -1
  115. package/dist/core/run-annotation.d.ts +73 -0
  116. package/dist/core/run-annotation.d.ts.map +1 -0
  117. package/dist/core/run-annotation.js +125 -0
  118. package/dist/core/run-annotation.js.map +1 -0
  119. package/dist/core/workflow-run.d.ts +4 -4
  120. package/dist/core/workflow-run.d.ts.map +1 -1
  121. package/dist/core/workflow-run.js +3 -3
  122. package/dist/core/workflow-run.js.map +1 -1
  123. package/dist/feature.d.ts +20 -16
  124. package/dist/feature.d.ts.map +1 -1
  125. package/dist/feature.js +5 -0
  126. package/dist/feature.js.map +1 -1
  127. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  128. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  129. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  130. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  131. package/dist/hooks/api/run-annotations.d.ts +25 -0
  132. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  133. package/dist/hooks/api/run-annotations.js +78 -0
  134. package/dist/hooks/api/run-annotations.js.map +1 -0
  135. package/dist/hooks/api/workflow-runs.d.ts +5 -3
  136. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  137. package/dist/hooks/api/workflow-runs.js +5 -4
  138. package/dist/hooks/api/workflow-runs.js.map +1 -1
  139. package/dist/index.d.ts +1 -0
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +1 -0
  142. package/dist/index.js.map +1 -1
  143. package/dist/pages/workflow-job-detail-page.d.ts +11 -0
  144. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
  145. package/dist/pages/workflow-job-detail-page.js +66 -0
  146. package/dist/pages/workflow-job-detail-page.js.map +1 -0
  147. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
  148. package/dist/pages/workflow-run-detail-page.js +4 -37
  149. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +52 -34
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +74 -43
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/routes/job-detail.d.ts +10 -0
  155. package/dist/routes/job-detail.d.ts.map +1 -0
  156. package/dist/routes/job-detail.js +27 -0
  157. package/dist/routes/job-detail.js.map +1 -0
  158. package/dist/routes/project-route.d.ts +1 -1
  159. package/dist/routes/run-detail.d.ts.map +1 -1
  160. package/dist/routes/runs.d.ts.map +1 -1
  161. package/dist/routes/workflows.d.ts.map +1 -1
  162. package/dist/tsconfig.test.tsbuildinfo +1 -1
  163. package/package.json +10 -9
  164. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
  165. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
  166. package/src/components/job-detail/job-detail-header.test.tsx +34 -0
  167. package/src/components/job-detail/job-detail-header.tsx +136 -0
  168. package/src/components/job-detail/job-detail-view.stories.tsx +531 -0
  169. package/src/components/job-detail/job-detail-view.tsx +467 -0
  170. package/src/components/job-detail/job-empty-states.test.ts +27 -0
  171. package/src/components/job-detail/job-empty-states.tsx +248 -0
  172. package/src/components/job-detail/job-selection.test.ts +188 -0
  173. package/src/components/job-detail/job-selection.ts +96 -0
  174. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
  175. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
  176. package/src/components/job-graph/job-graph-content.tsx +1 -1
  177. package/src/components/step-list/step-list-model.test.ts +1 -1
  178. package/src/components/step-list/step-list-model.ts +2 -2
  179. package/src/components/step-list/step-list.stories.tsx +1 -1
  180. package/src/components/step-list/step-list.test.tsx +33 -0
  181. package/src/components/step-list/step-list.tsx +40 -9
  182. package/src/components/workflow-run-duration-label.tsx +50 -12
  183. package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
  184. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
  185. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
  186. package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
  187. package/src/components/workflow-run-tabs/index.ts +3 -3
  188. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  189. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  190. package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
  191. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
  193. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
  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/index.ts +0 -1
  197. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +286 -0
  198. package/src/components/workflow-run-view/run-workspace-nav.tsx +314 -0
  199. package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
  200. package/src/components/workflow-run-view/workflow-run-view.test.tsx +237 -1085
  201. package/src/components/workflow-run-view/workflow-run-view.tsx +450 -323
  202. package/src/components/workflow-status/status-visuals.ts +5 -2
  203. package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
  204. package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
  205. package/src/core/entities/job-execution.ts +17 -2
  206. package/src/core/entities/step.ts +26 -0
  207. package/src/core/entities/workflow-run-display.test.ts +159 -0
  208. package/src/core/entities/workflow-run.ts +139 -1
  209. package/src/core/run-annotation.test.ts +315 -0
  210. package/src/core/run-annotation.ts +238 -0
  211. package/src/core/workflow-run.ts +16 -1
  212. package/src/feature.ts +5 -0
  213. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  214. package/src/hooks/api/run-annotations.ts +137 -0
  215. package/src/hooks/api/workflow-runs.ts +6 -2
  216. package/src/index.ts +1 -0
  217. package/src/page-harness-budget.test.ts +1 -0
  218. package/src/pages/workflow-job-detail-page.test.tsx +605 -0
  219. package/src/pages/workflow-job-detail-page.tsx +84 -0
  220. package/src/pages/workflow-run-detail-page.tsx +3 -43
  221. package/src/pages/workflow-run-pages.test.tsx +77 -332
  222. package/src/routes/inputs.test.ts +56 -6
  223. package/src/routes/inputs.ts +71 -19
  224. package/src/routes/job-detail.tsx +29 -0
  225. package/test/pages.tsx +25 -3
  226. package/test/render.tsx +6 -1
  227. package/tsconfig.build.tsbuildinfo +1 -1
  228. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
  229. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
  230. package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
  231. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
  232. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
  233. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
  234. package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
  235. package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
  236. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
  237. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
  238. package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
  239. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
  240. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
  241. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
  242. package/dist/components/workflow-run-view/job-card.d.ts +0 -17
  243. package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
  244. package/dist/components/workflow-run-view/job-card.js +0 -496
  245. package/dist/components/workflow-run-view/job-card.js.map +0 -1
  246. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
  247. package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
  248. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
  249. package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
  250. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
  251. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
  252. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
  253. package/dist/core/workflow-run-tabs.d.ts +0 -9
  254. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  255. package/dist/core/workflow-run-tabs.js +0 -3
  256. package/dist/core/workflow-run-tabs.js.map +0 -1
  257. package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
  258. package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
  259. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
  260. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
  261. package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
  262. package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
  263. package/src/components/workflow-run-view/job-card.test.tsx +0 -115
  264. package/src/components/workflow-run-view/job-card.tsx +0 -614
  265. package/src/core/workflow-run-tabs.ts +0 -8
  266. /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
  267. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
  268. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
  269. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
  270. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
  271. /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
  272. /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
@@ -0,0 +1,467 @@
1
+ // biome-ignore-all lint/a11y/noRedundantRoles: the job log region keeps an explicit role for the public accessibility contract.
2
+ // biome-ignore-all lint/a11y/noNoninteractiveTabindex: the job log region is intentionally keyboard focusable.
3
+
4
+ import {ApiError} from '@shipfox/client-api';
5
+ import {QueryLoadError} from '@shipfox/client-ui';
6
+ import {EmptyState} from '@shipfox/react-ui/empty-state';
7
+ import {Icon} from '@shipfox/react-ui/icon';
8
+ import {Skeleton} from '@shipfox/react-ui/skeleton';
9
+ import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
10
+ import {Text} from '@shipfox/react-ui/typography';
11
+ import {Link} from '@tanstack/react-router';
12
+ import {type RefObject, useEffect, useRef} from 'react';
13
+ import {summarizeJobAnnotations} from '#core/run-annotation.js';
14
+ import type {Job, JobExecution} from '#core/workflow-run.js';
15
+ import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
16
+ import type {useWorkflowRunAttemptQuery} from '#hooks/api/workflow-runs.js';
17
+ import {
18
+ type WorkflowJobSearch,
19
+ workflowJobSearchParams,
20
+ workflowRunSearchParams,
21
+ } from '#routes/inputs.js';
22
+ import {type StepExpandedContext, StepList} from '../step-list/index.js';
23
+ import {
24
+ WorkflowRunNotFound,
25
+ WorkflowRunStaleError,
26
+ } from '../workflow-run-view/workflow-run-states.js';
27
+ import {AgentConfigFailureCallout} from './agent-config-failure-callout.js';
28
+ import {JobDetailHeader} from './job-detail-header.js';
29
+ import {
30
+ CarriedOverStepPanel,
31
+ emptyStateForJob,
32
+ emptyStateForMissingExecution,
33
+ isAgentConfigFailure,
34
+ jobSucceededSummary,
35
+ toSelectedAttemptError,
36
+ } from './job-empty-states.js';
37
+ import {
38
+ resolveWorkflowJobSelection,
39
+ type WorkflowJobLandingSelection,
40
+ workflowJobLandingSelection,
41
+ } from './job-selection.js';
42
+ import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
43
+
44
+ export interface JobDetailViewProps {
45
+ workspaceSlug: string;
46
+ projectSlug: string;
47
+ workflowRunId: string;
48
+ jobId: string;
49
+ search: WorkflowJobSearch;
50
+ query: ReturnType<typeof useWorkflowRunAttemptQuery>;
51
+ newerAttempt?: number | undefined;
52
+ newerJob?: Job | undefined;
53
+ onSelectionChange: (selection: WorkflowJobSearch) => void;
54
+ }
55
+
56
+ export function JobDetailView({
57
+ workspaceSlug,
58
+ projectSlug,
59
+ workflowRunId,
60
+ jobId,
61
+ search,
62
+ query,
63
+ newerAttempt,
64
+ newerJob,
65
+ onSelectionChange,
66
+ }: JobDetailViewProps) {
67
+ const rootRef = useRef<HTMLDivElement>(null);
68
+ const pageScrollRef = useRef<HTMLDivElement>(null);
69
+ const landingSelectionRef = useRef<FrozenLandingSelection | undefined>(undefined);
70
+ const hasLoadedData = query.data !== undefined;
71
+ // Same query key as the run workspace, so this reads the cache instead of fetching again.
72
+ const annotations = useRunAnnotationsQuery({
73
+ workflowRunId,
74
+ runAttempt: query.data?.runAttempt.attempt,
75
+ });
76
+ const jobAnnotationSummary = annotations.annotations
77
+ ? summarizeJobAnnotations(annotations.annotations, jobId, {
78
+ truncated: annotations.summary?.truncated ?? false,
79
+ })
80
+ : undefined;
81
+
82
+ useEffect(() => {
83
+ if (!jobId || !hasLoadedData) return;
84
+ const heading = rootRef.current?.querySelector<HTMLElement>('[data-job-heading]');
85
+ // The heading is intentionally the route-change focus target: it announces the new job
86
+ // after rail navigation without putting focus on a decorative status element.
87
+ heading?.focus({preventScroll: true});
88
+ }, [hasLoadedData, jobId]);
89
+
90
+ if (query.isPending) return <JobDetailSkeleton />;
91
+
92
+ if (query.isError && query.data === undefined) {
93
+ if (query.error instanceof ApiError && query.error.status === 404) {
94
+ return <WorkflowRunNotFound />;
95
+ }
96
+ return <QueryLoadError query={query} subject="workflow run" icon="pulseLine" />;
97
+ }
98
+
99
+ if (query.data === undefined) return <JobDetailSkeleton />;
100
+
101
+ const run = query.data;
102
+ const job = run.jobs.find((candidate) => candidate.id === jobId);
103
+ if (!job) {
104
+ return (
105
+ <JobNotFoundState
106
+ workspaceSlug={workspaceSlug}
107
+ projectSlug={projectSlug}
108
+ workflowRunId={workflowRunId}
109
+ search={search}
110
+ />
111
+ );
112
+ }
113
+
114
+ const resolvedSelection = resolveWorkflowJobSelection({job, selection: search});
115
+ const selectedJobExecution = resolvedSelection.jobExecution;
116
+ const hasExplicitStep = Boolean(search.stepId && resolvedSelection.step);
117
+ const currentLandingSelection = workflowJobLandingSelection(selectedJobExecution);
118
+ if (selectedJobExecution) {
119
+ const frozenLanding = landingSelectionRef.current;
120
+ if (
121
+ !frozenLanding ||
122
+ frozenLanding.jobId !== job.id ||
123
+ frozenLanding.jobExecutionId !== selectedJobExecution.id
124
+ ) {
125
+ landingSelectionRef.current = {
126
+ jobId: job.id,
127
+ jobExecutionId: selectedJobExecution.id,
128
+ selection: currentLandingSelection,
129
+ hasSelection: currentLandingSelection !== undefined,
130
+ };
131
+ } else if (!frozenLanding.hasSelection && currentLandingSelection) {
132
+ landingSelectionRef.current = {
133
+ ...frozenLanding,
134
+ selection: currentLandingSelection,
135
+ hasSelection: true,
136
+ };
137
+ }
138
+ } else {
139
+ landingSelectionRef.current = undefined;
140
+ }
141
+ const landingSelection = landingSelectionRef.current?.selection;
142
+ const selectedAttemptId = hasExplicitStep ? resolvedSelection.selectedAttemptId : undefined;
143
+ const runningSelection = runningStepSelection(selectedJobExecution);
144
+ const selectedStepId = resolvedSelection.step?.id ?? landingSelection?.stepId;
145
+ const selectedAttemptForNotice = hasExplicitStep
146
+ ? resolvedSelection.selectedAttemptId
147
+ : landingSelection?.attemptId;
148
+ const showRetargetNotice =
149
+ runningSelection !== undefined &&
150
+ (selectedStepId !== runningSelection.stepId ||
151
+ selectedAttemptForNotice !== runningSelection.attemptId);
152
+ const succeededSummary =
153
+ !hasExplicitStep && landingSelection === undefined && selectedJobExecution
154
+ ? jobSucceededSummary(job, selectedJobExecution)
155
+ : undefined;
156
+
157
+ function selectExecution(jobExecutionId: string) {
158
+ onSelectionChange({
159
+ ...search,
160
+ jobExecutionId,
161
+ stepId: undefined,
162
+ stepAttemptId: undefined,
163
+ });
164
+ }
165
+
166
+ function selectAttempt(attemptId: string | undefined) {
167
+ if (!selectedJobExecution) return;
168
+ if (!attemptId) {
169
+ onSelectionChange({
170
+ ...search,
171
+ jobExecutionId: selectedJobExecution.id,
172
+ stepId: undefined,
173
+ stepAttemptId: undefined,
174
+ });
175
+ return;
176
+ }
177
+
178
+ const match = findAttempt(selectedJobExecution, attemptId);
179
+ if (!match) return;
180
+ onSelectionChange({
181
+ ...search,
182
+ jobExecutionId: selectedJobExecution.id,
183
+ stepId: match.step.id,
184
+ stepAttemptId: match.attemptId,
185
+ });
186
+ }
187
+
188
+ function retargetToRunningStep() {
189
+ if (!runningSelection || !selectedJobExecution) return;
190
+ onSelectionChange({
191
+ ...search,
192
+ jobExecutionId: selectedJobExecution.id,
193
+ stepId: runningSelection.stepId,
194
+ stepAttemptId: runningSelection.attemptId,
195
+ });
196
+ }
197
+
198
+ return (
199
+ <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
200
+ <div ref={rootRef} className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
201
+ {query.isError ? <WorkflowRunStaleError query={query} /> : null}
202
+ {newerJob && newerAttempt && newerAttempt > run.runAttempt.attempt ? (
203
+ <NewerAttemptNotice
204
+ workspaceSlug={workspaceSlug}
205
+ projectSlug={projectSlug}
206
+ runId={run.id}
207
+ jobId={newerJob.id}
208
+ attempt={newerAttempt}
209
+ />
210
+ ) : 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
+ >
216
+ <section className="min-w-0 overflow-hidden">
217
+ <JobDetailHeader
218
+ job={job}
219
+ selectedJobExecution={selectedJobExecution}
220
+ onSelectedJobExecutionChange={selectExecution}
221
+ workspaceSlug={workspaceSlug}
222
+ projectSlug={projectSlug}
223
+ workflowRunId={workflowRunId}
224
+ runAttempt={run.runAttempt.attempt}
225
+ annotationSummary={jobAnnotationSummary}
226
+ />
227
+ <Text as="h3" className="sr-only">
228
+ Logs
229
+ </Text>
230
+ {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
+ />
249
+ ) : (
250
+ <EmptyStateForMissingExecution job={job} />
251
+ )}
252
+ </section>
253
+ {showRetargetNotice && runningSelection ? (
254
+ <div
255
+ role="status"
256
+ 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"
258
+ >
259
+ <Text size="xs" className="min-w-0 text-foreground-neutral-muted">
260
+ Run moved on to{' '}
261
+ <span className="font-code text-foreground-neutral-base">
262
+ {runningSelection.stepLabel}
263
+ </span>
264
+ .
265
+ </Text>
266
+ <button
267
+ 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"
269
+ onClick={retargetToRunningStep}
270
+ >
271
+ Jump to it
272
+ </button>
273
+ </div>
274
+ ) : null}
275
+ {succeededSummary ? (
276
+ <Text size="xs" className="px-8 text-foreground-neutral-muted">
277
+ {succeededSummary}
278
+ </Text>
279
+ ) : null}
280
+ </section>
281
+ </div>
282
+ </div>
283
+ </TimeTickerProvider>
284
+ );
285
+ }
286
+
287
+ function ExpandedStep({
288
+ context,
289
+ workspaceSlug,
290
+ pageScrollRef,
291
+ }: {
292
+ context: StepExpandedContext;
293
+ workspaceSlug: string;
294
+ pageScrollRef: RefObject<HTMLDivElement | null>;
295
+ }) {
296
+ if (context.carriedOver) return <CarriedOverStepPanel />;
297
+
298
+ const selectedAttemptError =
299
+ toSelectedAttemptError(context.step, context.attemptError) ?? context.step.error;
300
+
301
+ return (
302
+ <section
303
+ role="region"
304
+ tabIndex={0}
305
+ 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"
307
+ >
308
+ {isAgentConfigFailure(context.step, selectedAttemptError) ? (
309
+ <AgentConfigFailureCallout
310
+ workspaceSlug={workspaceSlug}
311
+ config={context.step.agentConfig}
312
+ error={selectedAttemptError}
313
+ />
314
+ ) : null}
315
+ <StepAttemptLogPanel
316
+ stepId={context.stepId}
317
+ attempt={context.attempt}
318
+ attemptStatus={context.attemptStatus}
319
+ attemptStartedAt={context.attemptStartedAt}
320
+ pageScrollRef={pageScrollRef}
321
+ surfaceClassName="rounded-8"
322
+ />
323
+ </section>
324
+ );
325
+ }
326
+
327
+ function EmptyStateForMissingExecution({job}: {job: Job}) {
328
+ const emptyState = emptyStateForMissingExecution(job);
329
+ return (
330
+ <EmptyState
331
+ className="min-h-120 px-16 py-20"
332
+ icon="componentLine"
333
+ title={emptyState.title}
334
+ description={emptyState.description}
335
+ variant="compact"
336
+ />
337
+ );
338
+ }
339
+
340
+ function JobNotFoundState({
341
+ workspaceSlug,
342
+ projectSlug,
343
+ workflowRunId,
344
+ search,
345
+ }: {
346
+ workspaceSlug: string;
347
+ projectSlug: string;
348
+ workflowRunId: string;
349
+ search: WorkflowJobSearch;
350
+ }) {
351
+ return (
352
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-12 p-24">
353
+ <EmptyState
354
+ icon="componentLine"
355
+ title="Job not found"
356
+ description="This job is not part of the selected workflow run."
357
+ />
358
+ <Link
359
+ to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId"
360
+ params={{workspaceSlug, projectSlug, workflowRunId}}
361
+ search={
362
+ workflowRunSearchParams(
363
+ {runAttempt: search.runAttempt},
364
+ {runAttempt: search.runAttempt},
365
+ ) as never
366
+ }
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"
368
+ >
369
+ <Icon name="arrowLeftLine" size={14} aria-hidden="true" />
370
+ Back to run summary
371
+ </Link>
372
+ </div>
373
+ );
374
+ }
375
+
376
+ function NewerAttemptNotice({
377
+ workspaceSlug,
378
+ projectSlug,
379
+ runId,
380
+ jobId,
381
+ attempt,
382
+ }: {
383
+ workspaceSlug: string;
384
+ projectSlug: string;
385
+ runId: string;
386
+ jobId: string;
387
+ attempt: number;
388
+ }) {
389
+ 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">
392
+ <Text size="xs" className="text-foreground-neutral-muted">
393
+ A newer run attempt is available.
394
+ </Text>
395
+ <Link
396
+ to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId"
397
+ params={{workspaceSlug, projectSlug, workflowRunId: runId, jobId}}
398
+ 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"
400
+ >
401
+ View attempt #{attempt}
402
+ </Link>
403
+ </div>
404
+ </div>
405
+ );
406
+ }
407
+
408
+ function runningStepSelection(jobExecution: JobExecution | undefined) {
409
+ if (!jobExecution) return undefined;
410
+ const steps = [...jobExecution.steps].sort(
411
+ (left, right) => left.position - right.position || left.id.localeCompare(right.id),
412
+ );
413
+ for (let stepIndex = steps.length - 1; stepIndex >= 0; stepIndex -= 1) {
414
+ const step = steps[stepIndex];
415
+ if (!step) continue;
416
+ const attempt = [...step.attempts]
417
+ .sort((left, right) => left.attempt - right.attempt || left.id.localeCompare(right.id))
418
+ .reverse()
419
+ .find((candidate) => candidate.status === 'running');
420
+ if (attempt)
421
+ return {stepId: step.id, attemptId: attempt.id, stepLabel: step.name || step.key || step.id};
422
+ }
423
+ return undefined;
424
+ }
425
+
426
+ function findAttempt(jobExecution: JobExecution, attemptId: string) {
427
+ for (const step of jobExecution.steps) {
428
+ const attempt = step.attempts.find((candidate) => candidate.id === attemptId);
429
+ if (attempt) return {step, attemptId: attempt.id};
430
+ }
431
+ return undefined;
432
+ }
433
+
434
+ function JobDetailSkeleton() {
435
+ 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">
439
+ <Skeleton className="size-20 rounded-full" />
440
+ <Skeleton className="h-20 w-160 rounded-4" />
441
+ <Skeleton className="h-24 w-72 rounded-6" />
442
+ <Skeleton className="h-24 w-180 rounded-4" />
443
+ </header>
444
+ <div className="p-16">
445
+ {JOB_DETAIL_SKELETON_ROWS.map((row) => (
446
+ <div
447
+ key={row}
448
+ className="flex min-h-44 items-center gap-8 border-b border-border-neutral-base last:border-b-0"
449
+ >
450
+ <Skeleton className="size-14 rounded-full" />
451
+ <Skeleton className="h-16 w-180 rounded-4" />
452
+ </div>
453
+ ))}
454
+ </div>
455
+ </div>
456
+ </div>
457
+ );
458
+ }
459
+
460
+ const JOB_DETAIL_SKELETON_ROWS = ['step-1', 'step-2', 'step-3', 'step-4', 'step-5'];
461
+
462
+ interface FrozenLandingSelection {
463
+ jobId: string;
464
+ jobExecutionId: string;
465
+ selection: WorkflowJobLandingSelection | undefined;
466
+ hasSelection: boolean;
467
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ workflowJob,
3
+ workflowJobExecutionDto,
4
+ workflowStepDto,
5
+ } from '#test/fixtures/workflow-run.js';
6
+ import {jobSucceededSummary} from './job-empty-states.js';
7
+
8
+ describe('jobSucceededSummary', () => {
9
+ test('counts only succeeded steps when skipped steps are present', () => {
10
+ const job = workflowJob({
11
+ status: 'succeeded',
12
+ job_executions: [
13
+ workflowJobExecutionDto({
14
+ status: 'succeeded',
15
+ steps: [
16
+ workflowStepDto({status: 'succeeded'}),
17
+ workflowStepDto({status: 'skipped', position: 1}),
18
+ ],
19
+ }),
20
+ ],
21
+ });
22
+ const execution = job.jobExecutions[0];
23
+ if (!execution) throw new Error('Expected a job execution');
24
+
25
+ expect(jobSucceededSummary(job, execution)).toBe('1 step succeeded');
26
+ });
27
+ });