@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
@@ -1,7 +1,11 @@
1
1
  import {render, screen, waitFor, within} from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import type {workflowRunDetailDto} from '#test/fixtures/workflow-run.js';
4
- import {workflowJobDto, workflowRunDetail} from '#test/fixtures/workflow-run.js';
4
+ import {
5
+ workflowJobDto,
6
+ workflowJobExecutionDto,
7
+ workflowRunDetail,
8
+ } from '#test/fixtures/workflow-run.js';
5
9
  import {WorkflowRunSummary} from './workflow-run-summary.js';
6
10
 
7
11
  const RUN_ID = '66666666-6666-4666-8666-666666666666';
@@ -125,9 +129,9 @@ describe('WorkflowRunSummary', () => {
125
129
  const heading = await screen.findByRole('heading', {name: runName});
126
130
 
127
131
  expect(heading).toHaveAttribute('data-slot', 'tooltip-trigger');
128
- await waitFor(() =>
129
- expect(within(heading).getByText(runName)).toHaveAttribute('title', runName),
130
- );
132
+ // The Radix tooltip is the only name affordance: a native `title` beside it would fire a
133
+ // second, unstyled OS tooltip with the same text.
134
+ await waitFor(() => expect(within(heading).getByText(runName)).not.toHaveAttribute('title'));
131
135
  });
132
136
 
133
137
  test('shows the selected attempt duration, not the top-level run duration', async () => {
@@ -174,6 +178,61 @@ describe('WorkflowRunSummary', () => {
174
178
  expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
175
179
  });
176
180
 
181
+ test('reads a running run whose jobs have not started as queued', async () => {
182
+ vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
183
+ renderSummary({
184
+ jobs: [workflowJobDto({status: 'pending'})],
185
+ run_attempt: runningAttemptDto(),
186
+ });
187
+
188
+ const summary = await screen.findByRole('region', {name: 'deploy-web'});
189
+
190
+ expect(within(summary).getAllByText('Queued')).not.toHaveLength(0);
191
+ expect(within(summary).queryByText('Running')).not.toBeInTheDocument();
192
+ expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'queued 2m 14s');
193
+ });
194
+
195
+ test('names the job a queued run is waiting on when there is more than one', async () => {
196
+ renderSummary({
197
+ jobs: [
198
+ workflowJobDto({
199
+ name: 'lint',
200
+ status: 'pending',
201
+ job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
202
+ }),
203
+ workflowJobDto({
204
+ name: 'build',
205
+ status: 'pending',
206
+ job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T00:59:00.000Z'})],
207
+ }),
208
+ ],
209
+ run_attempt: runningAttemptDto(),
210
+ });
211
+
212
+ const summary = await screen.findByRole('region', {name: 'deploy-web'});
213
+
214
+ expect(within(summary).getByText('waiting on')).toBeInTheDocument();
215
+ expect(within(summary).getByText('build')).toBeInTheDocument();
216
+ });
217
+
218
+ test('leaves the waiting job unnamed when the run has a single job', async () => {
219
+ renderSummary({
220
+ jobs: [
221
+ workflowJobDto({
222
+ name: 'build',
223
+ status: 'pending',
224
+ job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
225
+ }),
226
+ ],
227
+ run_attempt: runningAttemptDto(),
228
+ });
229
+
230
+ const summary = await screen.findByRole('region', {name: 'deploy-web'});
231
+
232
+ expect(within(summary).getAllByText('Queued')).not.toHaveLength(0);
233
+ expect(within(summary).queryByText('waiting on')).not.toBeInTheDocument();
234
+ });
235
+
177
236
  test('does not render a whole-run source control', async () => {
178
237
  renderSummary();
179
238
 
@@ -250,7 +309,9 @@ describe('WorkflowRunSummary', () => {
250
309
  const onRerun = vi.fn();
251
310
  renderSummary({status: 'failed', jobs: [workflowJobDto({status: 'failed'})]}, {onRerun});
252
311
 
253
- await user.click(await screen.findByRole('button', {name: 'Re-run jobs'}));
312
+ const rerunButton = await screen.findByRole('button', {name: 'Re-run jobs'});
313
+ expect(rerunButton).toHaveClass('bg-background-neutral-base');
314
+ await user.click(rerunButton);
254
315
  expect(await screen.findByRole('menuitem', {name: 'Re-run all jobs'})).toBeInTheDocument();
255
316
  await user.click(await screen.findByRole('menuitem', {name: 'Re-run failed jobs'}));
256
317
 
@@ -328,6 +389,19 @@ function renderSummary(
328
389
  render(<WorkflowRunSummary run={run} {...props} />);
329
390
  }
330
391
 
392
+ function runningAttemptDto() {
393
+ return {
394
+ id: '11111111-1111-4111-8111-000000000001',
395
+ workflow_run_id: RUN_ID,
396
+ attempt: 1,
397
+ status: 'running' as const,
398
+ created_at: '2026-05-07T01:01:00.000Z',
399
+ started_at: '2026-05-07T01:00:00.000Z',
400
+ finished_at: null,
401
+ rerun_mode: null,
402
+ };
403
+ }
404
+
331
405
  function setElementWidths({scrollWidth, clientWidth}: {scrollWidth: number; clientWidth: number}) {
332
406
  Object.defineProperty(window.HTMLElement.prototype, 'scrollWidth', {
333
407
  configurable: true,
@@ -11,8 +11,7 @@ import {useIsTextTruncated} from '@shipfox/react-ui/hooks';
11
11
  import {RelativeTime} from '@shipfox/react-ui/relative-time';
12
12
  import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
13
13
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
14
- import {Header, Text} from '@shipfox/react-ui/typography';
15
- import {Link} from '@tanstack/react-router';
14
+ import {Code, Header, Text} from '@shipfox/react-ui/typography';
16
15
  import {useId} from 'react';
17
16
  import {WorkflowRunNumberLabel} from '#components/workflow-run-number-label.js';
18
17
  import {
@@ -21,15 +20,20 @@ import {
21
20
  WORKFLOW_RUN_STATUSES,
22
21
  type WorkflowRunDetail,
23
22
  type WorkflowRunRerunMode,
23
+ workflowRunBlockingJob,
24
+ workflowRunDetailDisplay,
24
25
  } from '#core/workflow-run.js';
25
- import {validateWorkflowRunsSearch, workflowRunListSearchParams} from '#routes/inputs.js';
26
26
  import {WorkflowRunDurationLabel} from '../workflow-run-duration-label.js';
27
27
  import {getWorkflowStatusVisual} from '../workflow-status/status-visuals.js';
28
28
  import {WorkflowRunAttemptSwitcher} from './workflow-run-attempt-switcher.js';
29
29
 
30
30
  const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
31
- ...WORKFLOW_RUN_STATUSES.map((status) => getWorkflowStatusVisual(status).label.length),
31
+ ...[...WORKFLOW_RUN_STATUSES, 'queued' as const].map(
32
+ (status) => getWorkflowStatusVisual(status).label.length,
33
+ ),
32
34
  );
35
+ const RERUN_BUTTON_SURFACE_CLASS_NAME =
36
+ 'bg-background-neutral-base hover:bg-background-neutral-hover active:bg-background-neutral-pressed';
33
37
 
34
38
  type WorkflowRunAction = 'cancel' | 'rerun-all' | 'rerun-menu' | 'none';
35
39
 
@@ -55,14 +59,19 @@ export function WorkflowRunSummary({
55
59
  latestAttempt,
56
60
  }: WorkflowRunSummaryProps) {
57
61
  const headingId = useId();
58
- const status = getWorkflowStatusVisual(run.runAttempt.status);
62
+ const display = workflowRunDetailDisplay(run);
63
+ const status = getWorkflowStatusVisual(display.status);
59
64
  const action = workflowRunActionForRun(run);
60
65
  const hasAction = canRenderWorkflowRunAction(action, onCancel, onRerun);
61
66
  const attemptSwitcher =
62
67
  latestAttempt && latestAttempt > 1 && workspaceSlug && projectSlug
63
68
  ? {workspaceSlug, projectSlug, latestAttempt}
64
69
  : null;
65
- const displayDuration = run.runAttempt.displayDuration;
70
+ const displayDuration = display.duration;
71
+ // Named only while the run is waiting and has more than one job: with a single job the rail
72
+ // and the graph already say which one it is.
73
+ const blockingJob =
74
+ display.status === 'queued' && run.jobs.length > 1 ? workflowRunBlockingJob(run.jobs) : null;
66
75
  const {ref: headingTextRef, isTruncated: isHeadingTruncated} =
67
76
  useIsTextTruncated<HTMLSpanElement>(run.name);
68
77
 
@@ -70,61 +79,32 @@ export function WorkflowRunSummary({
70
79
  <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
71
80
  <section aria-labelledby={headingId} className="bg-background-neutral-background px-16 py-12">
72
81
  <div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-12 gap-y-8 overflow-hidden max-[480px]:grid-cols-1">
73
- <nav
74
- aria-label="Breadcrumb"
75
- className="col-start-1 row-start-1 min-w-0 max-[480px]:col-start-auto max-[480px]:row-start-auto"
76
- >
77
- <ol className="flex min-w-0 items-center gap-8">
78
- {workspaceSlug && projectSlug ? (
79
- <li className="shrink-0">
80
- <Link
81
- to="/w/$workspaceSlug/p/$projectSlug/runs"
82
- params={{workspaceSlug, projectSlug}}
83
- search={
84
- ((previous: Record<string, unknown>) =>
85
- workflowRunListSearchParams(validateWorkflowRunsSearch(previous))) as never
86
- }
87
- className="rounded-4 text-xs font-medium text-foreground-neutral-subtle underline decoration-border-neutral-strong underline-offset-4 outline-none hover:text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active"
88
- >
89
- Runs
90
- </Link>
91
- </li>
92
- ) : null}
93
- {workspaceSlug && projectSlug ? (
94
- <li aria-hidden="true" className="shrink-0 text-xs text-foreground-neutral-subtle">
95
- /
96
- </li>
97
- ) : null}
98
- <li aria-current="page" className="flex min-w-0 items-center gap-8">
99
- <Badge variant={status.badge} size="xs">
100
- <span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
101
- {status.label}
102
- </span>
103
- </Badge>
82
+ <div className="col-start-1 row-start-1 min-w-0 max-[480px]:col-start-auto max-[480px]:row-start-auto">
83
+ <div className="flex min-w-0 items-center gap-8">
84
+ <Badge variant={status.badge} size="xs">
85
+ <span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
86
+ {status.label}
87
+ </span>
88
+ </Badge>
104
89
 
105
- <Tooltip>
106
- <TooltipTrigger asChild>
107
- <Header id={headingId} variant="h3" className="min-w-0 truncate">
108
- <span
109
- ref={headingTextRef}
110
- title={isHeadingTruncated ? run.name : undefined}
111
- className="block min-w-0 truncate"
112
- >
113
- {run.name}
114
- </span>
115
- </Header>
116
- </TooltipTrigger>
117
- {isHeadingTruncated ? (
118
- <TooltipContent>
119
- <Text as="span" size="xs" className="max-w-[360px] break-words">
120
- {run.name}
121
- </Text>
122
- </TooltipContent>
123
- ) : null}
124
- </Tooltip>
125
- </li>
126
- </ol>
127
- </nav>
90
+ <Tooltip>
91
+ <TooltipTrigger asChild>
92
+ <Header as="h1" id={headingId} variant="h3" className="min-w-0 truncate">
93
+ <span ref={headingTextRef} className="block min-w-0 truncate">
94
+ {run.name}
95
+ </span>
96
+ </Header>
97
+ </TooltipTrigger>
98
+ {isHeadingTruncated ? (
99
+ <TooltipContent>
100
+ <Text as="span" size="xs" className="max-w-[360px] break-words">
101
+ {run.name}
102
+ </Text>
103
+ </TooltipContent>
104
+ ) : null}
105
+ </Tooltip>
106
+ </div>
107
+ </div>
128
108
 
129
109
  {hasAction ? (
130
110
  <div className="col-start-2 row-start-1 flex min-w-max items-center gap-6 justify-self-end max-[480px]:col-start-auto max-[480px]:row-start-auto max-[480px]:justify-self-start">
@@ -204,6 +184,20 @@ export function WorkflowRunSummary({
204
184
  />
205
185
  </>
206
186
  ) : null}
187
+
188
+ {blockingJob ? (
189
+ <>
190
+ <MetadataSeparator />
191
+ <span className="flex min-w-0 items-center gap-4">
192
+ <Text as="span" size="xs" className="shrink-0">
193
+ waiting on
194
+ </Text>
195
+ <Code as="span" variant="label" className="min-w-0 truncate">
196
+ {blockingJob.displayName}
197
+ </Code>
198
+ </span>
199
+ </>
200
+ ) : null}
207
201
  </div>
208
202
  </div>
209
203
  </section>
@@ -251,6 +245,7 @@ function WorkflowRunActionSlot({
251
245
  type="button"
252
246
  variant="secondary"
253
247
  size="xs"
248
+ className={RERUN_BUTTON_SURFACE_CLASS_NAME}
254
249
  isLoading={rerunPending}
255
250
  disabled={rerunPending}
256
251
  onClick={() => onRerun('all')}
@@ -269,6 +264,7 @@ function WorkflowRunActionSlot({
269
264
  type="button"
270
265
  variant="secondary"
271
266
  size="xs"
267
+ className={RERUN_BUTTON_SURFACE_CLASS_NAME}
272
268
  iconRight="arrowDownSLine"
273
269
  isLoading={rerunPending}
274
270
  disabled={rerunPending}
@@ -1,5 +1,5 @@
1
+ export {RunAnnotationCountChip} from './run-annotation-count-chip.js';
2
+ export {annotationElementId, RunAnnotationItem} from './run-annotation-item.js';
3
+ export {RunAnnotationList, type RunAnnotationListQuery} from './run-annotation-list.js';
1
4
  export {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
2
5
  export {RunAnnotationsEmpty} from './run-annotations-empty.js';
3
- export {RunJobsList} from './run-jobs-list.js';
4
- export {RunTabCount} from './run-tab-count.js';
5
- export {RunTabStrip} from './run-tab-strip.js';
@@ -0,0 +1,63 @@
1
+ import {Icon} from '@shipfox/react-ui/icon';
2
+ import {cn} from '@shipfox/react-ui/utils';
3
+ import {Link} from '@tanstack/react-router';
4
+ import {highestRunAnnotationSeverity, type RunAnnotationSummary} from '#core/run-annotation.js';
5
+ import {type WorkflowRunsSearch, workflowRunSearchParams} from '#routes/inputs.js';
6
+ import {SEVERITY_CHIP_TONE, SEVERITY_ICON} from './severity-visuals.js';
7
+
8
+ export interface RunAnnotationCountChipProps {
9
+ summary: RunAnnotationSummary | undefined;
10
+ workspaceSlug: string;
11
+ projectSlug: string;
12
+ workflowRunId: string;
13
+ runAttempt?: number | undefined;
14
+ /** Scopes the destination list to one job. */
15
+ jobId?: string | undefined;
16
+ }
17
+
18
+ /**
19
+ * A bounded reference into the run's Annotations section: a count, its highest severity, and one
20
+ * link. Never a body, so an annotation is rendered exactly once per page.
21
+ */
22
+ export function RunAnnotationCountChip({
23
+ summary,
24
+ workspaceSlug,
25
+ projectSlug,
26
+ workflowRunId,
27
+ runAttempt,
28
+ jobId,
29
+ }: RunAnnotationCountChipProps) {
30
+ if (!summary || summary.total === 0) return null;
31
+
32
+ const severity = highestRunAnnotationSeverity(summary);
33
+ const count = `${summary.total}${summary.truncated ? '+' : ''}`;
34
+ const label = `${count} ${summary.total === 1 && !summary.truncated ? 'annotation' : 'annotations'}`;
35
+ const accessibleLabel = severity
36
+ ? `View ${label}, highest severity ${severity}`
37
+ : `View ${label}`;
38
+ const search: WorkflowRunsSearch = {tab: 'annotations', ...(runAttempt ? {runAttempt} : {})};
39
+ if (jobId) search.jobId = jobId;
40
+
41
+ return (
42
+ <Link
43
+ to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId"
44
+ params={{workspaceSlug, projectSlug, workflowRunId}}
45
+ search={workflowRunSearchParams(search, search) as never}
46
+ aria-label={accessibleLabel}
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',
49
+ // The chip stays 20px optically so it sits level with the duration metadata beside it;
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',
52
+ severity ? SEVERITY_CHIP_TONE[severity] : 'border-border-neutral-base',
53
+ )}
54
+ >
55
+ <Icon
56
+ name={severity ? SEVERITY_ICON[severity] : 'fileTextLine'}
57
+ size={12}
58
+ aria-hidden="true"
59
+ />
60
+ <span className="text-foreground-neutral-base">{count}</span>
61
+ </Link>
62
+ );
63
+ }
@@ -0,0 +1,123 @@
1
+ import {AnnotationCard} from '@shipfox/client-ui';
2
+ import {Button} from '@shipfox/react-ui/button';
3
+ import {Icon} from '@shipfox/react-ui/icon';
4
+ import {Text} from '@shipfox/react-ui/typography';
5
+ import {cn} from '@shipfox/react-ui/utils';
6
+ import {Link} from '@tanstack/react-router';
7
+ import {useEffect, useRef} from 'react';
8
+ import type {RunAnnotationEntry} from '#core/run-annotation.js';
9
+ import {workflowJobSearchParams} from '#routes/inputs.js';
10
+
11
+ export interface RunAnnotationItemProps {
12
+ entry: RunAnnotationEntry;
13
+ workspaceSlug?: string | undefined;
14
+ projectSlug?: string | undefined;
15
+ workflowRunId: string;
16
+ runAttempt?: number | undefined;
17
+ /** Deep-link target from `?annotation=`, which takes focus rather than only being scrolled to. */
18
+ selected?: boolean | undefined;
19
+ }
20
+
21
+ /**
22
+ * One annotation, and the only place in the product that renders an annotation body.
23
+ *
24
+ * The title is the annotation's `context`, which is what the emitting step named the block.
25
+ * Everything below it exists so a reader can get from a diagnostic back to its cause.
26
+ */
27
+ export function RunAnnotationItem({
28
+ entry,
29
+ workspaceSlug,
30
+ projectSlug,
31
+ workflowRunId,
32
+ runAttempt,
33
+ selected = false,
34
+ }: RunAnnotationItemProps) {
35
+ const {annotation, origin} = entry;
36
+ const canLink = Boolean(origin && workspaceSlug && projectSlug);
37
+ const itemRef = useRef<HTMLLIElement>(null);
38
+ const focusedRef = useRef(false);
39
+
40
+ useEffect(() => {
41
+ if (!selected) {
42
+ focusedRef.current = false;
43
+ return;
44
+ }
45
+ if (focusedRef.current) return;
46
+
47
+ // A deep link that only scrolls leaves a keyboard or screen-reader user at the document
48
+ // start while the page moves under them.
49
+ focusedRef.current = true;
50
+ itemRef.current?.focus({preventScroll: true});
51
+ itemRef.current?.scrollIntoView({block: 'nearest'});
52
+ }, [selected]);
53
+
54
+ return (
55
+ <li
56
+ ref={itemRef}
57
+ tabIndex={-1}
58
+ aria-current={selected ? 'true' : undefined}
59
+ className={cn(
60
+ 'rounded-8 outline-none focus-visible:shadow-border-interactive-with-active',
61
+ selected && 'shadow-border-interactive-with-active',
62
+ )}
63
+ >
64
+ <AnnotationCard
65
+ id={annotationElementId(annotation.id)}
66
+ style={annotation.style}
67
+ title={annotation.context}
68
+ titleAs="h3"
69
+ provenance={<RunAnnotationProvenance entry={entry} />}
70
+ body={annotation.body}
71
+ action={
72
+ canLink && origin ? (
73
+ <Button
74
+ asChild
75
+ size="xs"
76
+ variant="transparent"
77
+ className="[@media(pointer:coarse)]:min-h-44"
78
+ >
79
+ <Link
80
+ to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId"
81
+ params={{
82
+ workspaceSlug: workspaceSlug ?? '',
83
+ projectSlug: projectSlug ?? '',
84
+ workflowRunId,
85
+ jobId: origin.jobId,
86
+ }}
87
+ search={
88
+ workflowJobSearchParams({
89
+ jobExecutionId: origin.jobExecutionId,
90
+ stepId: origin.stepId,
91
+ stepAttemptId: origin.stepAttemptId,
92
+ runAttempt,
93
+ }) as never
94
+ }
95
+ >
96
+ Open step
97
+ <Icon name="arrowRightLine" size={12} aria-hidden="true" />
98
+ </Link>
99
+ </Button>
100
+ ) : null
101
+ }
102
+ />
103
+ </li>
104
+ );
105
+ }
106
+
107
+ /** `job · execution #2 · run tests · attempt 1`, dropping any part the run no longer resolves. */
108
+ function RunAnnotationProvenance({entry}: {entry: RunAnnotationEntry}) {
109
+ const parts = [entry.jobName, entry.executionLabel, entry.stepLabel, entry.attemptLabel].filter(
110
+ (part): part is string => Boolean(part),
111
+ );
112
+
113
+ return (
114
+ <Text as="p" size="xs" className="min-w-0 break-words font-code text-foreground-neutral-subtle">
115
+ {parts.join(' · ')}
116
+ </Text>
117
+ );
118
+ }
119
+
120
+ /** Stable element id so `?annotation=<id>` can scroll to and highlight one annotation. */
121
+ export function annotationElementId(annotationId: string): string {
122
+ return `run-annotation-${annotationId}`;
123
+ }