@shipfox/client-workflows 12.0.2 → 13.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 (232) hide show
  1. package/.storybook/preview.tsx +3 -3
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/dist/components/job-graph/job-duration-format.d.ts +2 -2
  5. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
  6. package/dist/components/job-graph/job-duration-format.js +4 -2
  7. package/dist/components/job-graph/job-duration-format.js.map +1 -1
  8. package/dist/components/job-graph/job-node.d.ts +1 -1
  9. package/dist/components/job-graph/job-node.d.ts.map +1 -1
  10. package/dist/components/job-graph/job-node.js +5 -4
  11. package/dist/components/job-graph/job-node.js.map +1 -1
  12. package/dist/components/step-list/step-list.d.ts +2 -2
  13. package/dist/components/step-list/step-list.d.ts.map +1 -1
  14. package/dist/components/step-list/step-list.js.map +1 -1
  15. package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
  16. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
  17. package/dist/components/workflow-run-list/job-status-strip.js +185 -0
  18. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
  19. package/dist/components/workflow-run-list/run-display.d.ts +27 -3
  20. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
  21. package/dist/components/workflow-run-list/run-display.js +78 -7
  22. package/dist/components/workflow-run-list/run-display.js.map +1 -1
  23. package/dist/components/workflow-run-list/types.d.ts +23 -24
  24. package/dist/components/workflow-run-list/types.d.ts.map +1 -1
  25. package/dist/components/workflow-run-list/types.js.map +1 -1
  26. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
  27. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
  28. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
  29. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
  30. package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
  31. package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
  32. package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
  33. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
  34. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
  35. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  36. package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
  37. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  38. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
  39. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  40. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
  41. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  42. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
  43. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  44. package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
  45. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  46. package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
  47. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
  48. package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
  49. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
  50. package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
  51. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  52. package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
  53. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  54. package/dist/components/workflow-run-number-label.d.ts +15 -0
  55. package/dist/components/workflow-run-number-label.d.ts.map +1 -0
  56. package/dist/components/workflow-run-number-label.js +41 -0
  57. package/dist/components/workflow-run-number-label.js.map +1 -0
  58. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
  59. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
  60. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +8 -7
  61. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  62. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +3 -3
  63. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  64. package/dist/components/workflow-run-summary/workflow-run-summary.js +16 -7
  65. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  66. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
  67. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
  68. package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
  69. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
  70. package/dist/components/workflow-run-view/job-card.d.ts +2 -2
  71. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
  72. package/dist/components/workflow-run-view/job-card.js +51 -32
  73. package/dist/components/workflow-run-view/job-card.js.map +1 -1
  74. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
  75. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
  76. package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
  77. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
  78. package/dist/components/workflow-run-view/workflow-run-view.d.ts +3 -2
  79. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  80. package/dist/components/workflow-run-view/workflow-run-view.js +15 -11
  81. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  82. package/dist/components/workflow-status/status-visuals.d.ts +4 -4
  83. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  84. package/dist/components/workflow-status/status-visuals.js +7 -0
  85. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  86. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
  87. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  88. package/dist/components/workflow-status/workflow-status-icon.js +6 -4
  89. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  90. package/dist/core/entities/job-execution.d.ts +10 -0
  91. package/dist/core/entities/job-execution.d.ts.map +1 -1
  92. package/dist/core/entities/job-execution.js +11 -0
  93. package/dist/core/entities/job-execution.js.map +1 -1
  94. package/dist/core/entities/job.d.ts +4 -2
  95. package/dist/core/entities/job.d.ts.map +1 -1
  96. package/dist/core/entities/job.js +8 -1
  97. package/dist/core/entities/job.js.map +1 -1
  98. package/dist/core/entities/workflow-run.d.ts +52 -4
  99. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  100. package/dist/core/entities/workflow-run.js +22 -3
  101. package/dist/core/entities/workflow-run.js.map +1 -1
  102. package/dist/core/workflow-run.d.ts +6 -6
  103. package/dist/core/workflow-run.d.ts.map +1 -1
  104. package/dist/core/workflow-run.js +3 -3
  105. package/dist/core/workflow-run.js.map +1 -1
  106. package/dist/feature.d.ts +7 -7
  107. package/dist/feature.js +5 -5
  108. package/dist/feature.js.map +1 -1
  109. package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
  110. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  111. package/dist/hooks/api/workflow-run-mapper.js +31 -3
  112. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  113. package/dist/hooks/api/workflow-runs.d.ts +4 -4
  114. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  115. package/dist/hooks/api/workflow-runs.js +12 -4
  116. package/dist/hooks/api/workflow-runs.js.map +1 -1
  117. package/dist/index.d.ts +4 -3
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -2
  120. package/dist/index.js.map +1 -1
  121. package/dist/pages/project-workflows-page.js +46 -1
  122. package/dist/pages/project-workflows-page.js.map +1 -1
  123. package/dist/pages/workflow-run-detail-page.d.ts +11 -0
  124. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
  125. package/dist/pages/workflow-run-detail-page.js +30 -0
  126. package/dist/pages/workflow-run-detail-page.js.map +1 -0
  127. package/dist/pages/workflow-run-list-page.d.ts +10 -0
  128. package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
  129. package/dist/pages/workflow-run-list-page.js +43 -0
  130. package/dist/pages/workflow-run-list-page.js.map +1 -0
  131. package/dist/routes/inputs.d.ts +61 -5
  132. package/dist/routes/inputs.d.ts.map +1 -1
  133. package/dist/routes/inputs.js +150 -13
  134. package/dist/routes/inputs.js.map +1 -1
  135. package/dist/routes/project-route.d.ts +6 -0
  136. package/dist/routes/project-route.d.ts.map +1 -0
  137. package/dist/routes/project-route.js +25 -0
  138. package/dist/routes/project-route.js.map +1 -0
  139. package/dist/routes/run-detail.d.ts.map +1 -1
  140. package/dist/routes/run-detail.js +12 -7
  141. package/dist/routes/run-detail.js.map +1 -1
  142. package/dist/routes/runs.d.ts.map +1 -1
  143. package/dist/routes/runs.js +11 -6
  144. package/dist/routes/runs.js.map +1 -1
  145. package/dist/routes/workflows.d.ts.map +1 -1
  146. package/dist/routes/workflows.js +6 -5
  147. package/dist/routes/workflows.js.map +1 -1
  148. package/dist/tsconfig.test.tsbuildinfo +1 -1
  149. package/package.json +9 -9
  150. package/src/components/job-graph/job-duration-format.ts +11 -2
  151. package/src/components/job-graph/job-graph-view.test.tsx +19 -8
  152. package/src/components/job-graph/job-graph.stories.tsx +23 -19
  153. package/src/components/job-graph/job-node.stories.tsx +10 -5
  154. package/src/components/job-graph/job-node.test.tsx +55 -10
  155. package/src/components/job-graph/job-node.tsx +16 -9
  156. package/src/components/step-list/step-list.stories.tsx +2 -3
  157. package/src/components/step-list/step-list.tsx +3 -2
  158. package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
  159. package/src/components/workflow-run-list/run-display.test.ts +169 -30
  160. package/src/components/workflow-run-list/run-display.ts +116 -9
  161. package/src/components/workflow-run-list/types.ts +21 -17
  162. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
  163. package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
  164. package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
  165. package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
  166. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
  167. package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
  168. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
  169. package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
  170. package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
  171. package/src/components/workflow-run-number-label.test.tsx +45 -0
  172. package/src/components/workflow-run-number-label.tsx +47 -0
  173. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
  174. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +13 -12
  175. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +6 -8
  176. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +27 -2
  177. package/src/components/workflow-run-summary/workflow-run-summary.tsx +19 -9
  178. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
  179. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
  180. package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
  181. package/src/components/workflow-run-view/job-card.test.tsx +115 -0
  182. package/src/components/workflow-run-view/job-card.tsx +74 -35
  183. package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
  184. package/src/components/workflow-run-view/workflow-run-view.test.tsx +88 -30
  185. package/src/components/workflow-run-view/workflow-run-view.tsx +24 -14
  186. package/src/components/workflow-status/status-visuals.test.ts +9 -0
  187. package/src/components/workflow-status/status-visuals.ts +6 -4
  188. package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
  189. package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
  190. package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
  191. package/src/core/entities/job-execution.ts +22 -0
  192. package/src/core/entities/job-status.test.ts +104 -0
  193. package/src/core/entities/job.ts +18 -3
  194. package/src/core/entities/workflow-run.ts +76 -7
  195. package/src/core/workflow-run.test.ts +24 -9
  196. package/src/core/workflow-run.ts +17 -2
  197. package/src/feature.ts +5 -5
  198. package/src/hooks/api/workflow-run-mapper.ts +30 -3
  199. package/src/hooks/api/workflow-runs.test.tsx +2 -0
  200. package/src/hooks/api/workflow-runs.ts +12 -7
  201. package/src/index.ts +7 -2
  202. package/src/page-harness-budget.test.ts +1 -1
  203. package/src/pages/project-workflows-page.test.tsx +59 -5
  204. package/src/pages/project-workflows-page.tsx +34 -1
  205. package/src/pages/workflow-run-detail-page.tsx +45 -0
  206. package/src/pages/workflow-run-list-page.tsx +65 -0
  207. package/src/pages/{workflow-run-page.test.tsx → workflow-run-pages.test.tsx} +211 -47
  208. package/src/routes/inputs.test.ts +193 -12
  209. package/src/routes/inputs.ts +170 -11
  210. package/src/routes/project-route.tsx +25 -0
  211. package/src/routes/run-detail.tsx +15 -8
  212. package/src/routes/runs.tsx +14 -7
  213. package/src/routes/workflows.tsx +5 -4
  214. package/test/fixtures/workflow-run.ts +76 -7
  215. package/test/pages.tsx +18 -8
  216. package/test/render.tsx +8 -2
  217. package/tsconfig.build.tsbuildinfo +1 -1
  218. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
  219. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
  220. package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
  221. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
  222. package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
  223. package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
  224. package/dist/pages/workflow-run-first-time-use.js +0 -17
  225. package/dist/pages/workflow-run-first-time-use.js.map +0 -1
  226. package/dist/pages/workflow-run-page.d.ts +0 -10
  227. package/dist/pages/workflow-run-page.d.ts.map +0 -1
  228. package/dist/pages/workflow-run-page.js +0 -90
  229. package/dist/pages/workflow-run-page.js.map +0 -1
  230. package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
  231. package/src/pages/workflow-run-first-time-use.tsx +0 -17
  232. package/src/pages/workflow-run-page.tsx +0 -94
@@ -15,7 +15,7 @@ import {
15
15
  workflowStepAttemptDto,
16
16
  workflowStepDto,
17
17
  } from '#test/fixtures/workflow-run.js';
18
- import {jsonResponse, PROJECT_TEST_WID, renderProjectPage} from '#test/pages.js';
18
+ import {jsonResponse, PROJECT_TEST_WSLUG, renderProjectPage} from '#test/pages.js';
19
19
  import {WorkflowRunView} from './workflow-run-view.js';
20
20
 
21
21
  const RUN_ID = '66666666-6666-4666-8666-666666666666';
@@ -29,6 +29,9 @@ const RERUN_BUTTON_NAME = /^Re-run/;
29
29
  const ATTEMPT_2_PATTERN = /Attempt 2/;
30
30
  const COPY_RUN_BUTTON_NAME = /Copy run/;
31
31
  const EXECUTION_ONE_MENU_ITEM_PATTERN = /#1/;
32
+ const LISTENING_EXECUTION_SWITCHER_NAME =
33
+ 'Switch job execution, currently execution 2: Deployment status event';
34
+ const JOB_TEMPLATE_NAME = ['Implement $', '{{ event.issue.identifier }}'].join('');
32
35
 
33
36
  describe('WorkflowRunView', () => {
34
37
  test('renders the run summary, jobs graph, and selected job step attempts when a run loads', async () => {
@@ -55,6 +58,37 @@ describe('WorkflowRunView', () => {
55
58
  ).toBeInTheDocument();
56
59
  });
57
60
 
61
+ test('uses the selected job execution name as the job card title', async () => {
62
+ configureApiClient({
63
+ fetchImpl: vi.fn(() =>
64
+ Promise.resolve(
65
+ jsonResponse(
66
+ workflowRunViewDetailDto({
67
+ jobs: [
68
+ workflowJobDto({
69
+ id: BUILD_JOB_ID,
70
+ run_attempt_id: RUN_ID,
71
+ name: JOB_TEMPLATE_NAME,
72
+ job_executions: [
73
+ workflowJobExecutionDto({
74
+ job_id: BUILD_JOB_ID,
75
+ name: 'Implement ENG-123',
76
+ }),
77
+ ],
78
+ }),
79
+ ],
80
+ }),
81
+ ),
82
+ ),
83
+ ),
84
+ });
85
+
86
+ renderView();
87
+
88
+ expect(await screen.findByRole('region', {name: 'Implement ENG-123'})).toBeInTheDocument();
89
+ expect(screen.queryByRole('region', {name: JOB_TEMPLATE_NAME})).not.toBeInTheDocument();
90
+ });
91
+
58
92
  test('renders active step attempt logs inline when the selected job is running', async () => {
59
93
  const stepId = '99999999-9999-4999-8999-000000000001';
60
94
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
@@ -125,6 +159,7 @@ describe('WorkflowRunView', () => {
125
159
  id: '77777777-7777-4777-8777-000000000001',
126
160
  job_id: BUILD_JOB_ID,
127
161
  sequence: 1,
162
+ name: 'Pull request event',
128
163
  status: 'succeeded',
129
164
  trigger_events: [
130
165
  {
@@ -132,6 +167,10 @@ describe('WorkflowRunView', () => {
132
167
  event: 'pull_request',
133
168
  delivery_id: 'delivery-1',
134
169
  received_at: '2026-05-07T01:00:00.000Z',
170
+ project: null,
171
+ repository: null,
172
+ ref: null,
173
+ commit: null,
135
174
  data: {number: 12},
136
175
  },
137
176
  ],
@@ -155,6 +194,7 @@ describe('WorkflowRunView', () => {
155
194
  id: '77777777-7777-4777-8777-000000000002',
156
195
  job_id: BUILD_JOB_ID,
157
196
  sequence: 2,
197
+ name: 'Deployment status event',
158
198
  status: 'failed',
159
199
  status_reason: 'step_failed',
160
200
  trigger_events: [
@@ -163,6 +203,10 @@ describe('WorkflowRunView', () => {
163
203
  event: 'deployment_status',
164
204
  delivery_id: 'delivery-2',
165
205
  received_at: '2026-05-07T01:01:00.000Z',
206
+ project: null,
207
+ repository: null,
208
+ ref: null,
209
+ commit: null,
166
210
  data: {state: 'success'},
167
211
  },
168
212
  {
@@ -170,6 +214,10 @@ describe('WorkflowRunView', () => {
170
214
  event: 'check_run',
171
215
  delivery_id: 'delivery-3',
172
216
  received_at: '2026-05-07T01:01:10.000Z',
217
+ project: null,
218
+ repository: null,
219
+ ref: null,
220
+ commit: null,
173
221
  data: {status: 'completed'},
174
222
  },
175
223
  ],
@@ -200,18 +248,20 @@ describe('WorkflowRunView', () => {
200
248
  });
201
249
 
202
250
  renderView();
251
+ expect(
252
+ await screen.findByRole('region', {name: 'Deployment status event'}),
253
+ ).toBeInTheDocument();
203
254
  expect(await screen.findByText('second-event')).toBeInTheDocument();
204
255
  expect(
205
256
  await screen.findByRole('button', {name: 'github · deployment_status (2 events)'}),
206
257
  ).toBeInTheDocument();
207
258
  expect(screen.getByText('deployment_status (2)')).toBeInTheDocument();
208
259
 
209
- await user.click(
210
- screen.getByRole('button', {name: 'Switch job execution, currently execution 2'}),
211
- );
260
+ await user.click(screen.getByRole('button', {name: LISTENING_EXECUTION_SWITCHER_NAME}));
212
261
  await user.click(screen.getByRole('menuitem', {name: EXECUTION_ONE_MENU_ITEM_PATTERN}));
213
262
 
214
263
  expect(await screen.findByText('first-event')).toBeInTheDocument();
264
+ expect(screen.getByRole('region', {name: 'Pull request event'})).toBeInTheDocument();
215
265
  expect(screen.queryByText('second-event')).not.toBeInTheDocument();
216
266
  expect(await screen.findByRole('button', {name: 'github · pull_request'})).toBeInTheDocument();
217
267
  expect(screen.getByText('pull_request')).toBeInTheDocument();
@@ -301,7 +351,7 @@ describe('WorkflowRunView', () => {
301
351
  ).toBeInTheDocument();
302
352
  expect(screen.getByRole('link', {name: 'Configure Agents'})).toHaveAttribute(
303
353
  'href',
304
- `/workspaces/${PROJECT_TEST_WID}/settings/agents`,
354
+ `/w/${PROJECT_TEST_WSLUG}/settings/agents`,
305
355
  );
306
356
  });
307
357
 
@@ -378,7 +428,7 @@ describe('WorkflowRunView', () => {
378
428
  expect(screen.getByText('Configure credentials for anthropic')).toBeInTheDocument();
379
429
  expect(screen.getByRole('link', {name: 'Configure Agents'})).toHaveAttribute(
380
430
  'href',
381
- `/workspaces/${PROJECT_TEST_WID}/settings/agents`,
431
+ `/w/${PROJECT_TEST_WSLUG}/settings/agents`,
382
432
  );
383
433
  });
384
434
 
@@ -413,7 +463,7 @@ describe('WorkflowRunView', () => {
413
463
  expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
414
464
  });
415
465
 
416
- test('uses the selected execution status for zero-step execution states', async () => {
466
+ test('keeps a terminal job status over inconsistent zero-step execution activity', async () => {
417
467
  configureApiClient({
418
468
  fetchImpl: vi.fn(() =>
419
469
  Promise.resolve(
@@ -445,11 +495,9 @@ describe('WorkflowRunView', () => {
445
495
 
446
496
  renderView();
447
497
 
448
- expect(await screen.findByText('Waiting for the first step')).toBeInTheDocument();
449
- expect(
450
- screen.getByText('This job is running, but no steps have started yet.'),
451
- ).toBeInTheDocument();
452
- expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
498
+ expect(await screen.findByText('No steps recorded')).toBeInTheDocument();
499
+ expect(screen.getByText('Execution #2 finished without recorded steps.')).toBeInTheDocument();
500
+ expect(screen.queryByText('Waiting for the first step')).not.toBeInTheDocument();
453
501
  });
454
502
 
455
503
  test('renders pending zero-step jobs as waiting to start', async () => {
@@ -480,7 +528,7 @@ describe('WorkflowRunView', () => {
480
528
  expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
481
529
  });
482
530
 
483
- test('renders running zero-step jobs as waiting for the first step', async () => {
531
+ test('renders claimed idle jobs as runner preparation', async () => {
484
532
  configureApiClient({
485
533
  fetchImpl: vi.fn(() =>
486
534
  Promise.resolve(
@@ -509,10 +557,13 @@ describe('WorkflowRunView', () => {
509
557
 
510
558
  renderView();
511
559
 
512
- expect(await screen.findByText('Waiting for the first step')).toBeInTheDocument();
560
+ expect(await screen.findByText('Runner preparing job')).toBeInTheDocument();
513
561
  expect(
514
- screen.getByText('This job is running, but no steps have started yet.'),
562
+ screen.getByText('A runner is assigned. Steps will appear here when work begins.'),
515
563
  ).toBeInTheDocument();
564
+ expect(
565
+ screen.queryByText('This job is running, but no steps have started yet.'),
566
+ ).not.toBeInTheDocument();
516
567
  expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
517
568
  });
518
569
 
@@ -898,7 +949,7 @@ describe('WorkflowRunView', () => {
898
949
  expect(successSpy).toHaveBeenCalledWith('Re-run started');
899
950
  await waitFor(() =>
900
951
  expect(router.state.location.pathname).toBe(
901
- `/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${rerunId}`,
952
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs/${rerunId}`,
902
953
  ),
903
954
  );
904
955
  expect((router.state.location.search as Record<string, unknown>).runAttempt).toBeUndefined();
@@ -1008,11 +1059,12 @@ describe('WorkflowRunView', () => {
1008
1059
  configureApiClient({fetchImpl: fetchImpl as typeof fetch});
1009
1060
 
1010
1061
  const {router} = renderProjectPage(
1011
- `/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${RUN_ID}?job=${BUILD_JOB_ID}&step=${CHECKOUT_STEP_ID}&stepAttempt=${CHECKOUT_ATTEMPT_ID}`,
1062
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}?job=${BUILD_JOB_ID}&step=${CHECKOUT_STEP_ID}&stepAttempt=${CHECKOUT_ATTEMPT_ID}`,
1012
1063
  () => (
1013
1064
  <WorkflowRunView
1014
- workspaceId={PROJECT_TEST_WID}
1015
1065
  projectId={PROJECT_ID}
1066
+ workspaceSlug={PROJECT_TEST_WSLUG}
1067
+ projectSlug="project"
1016
1068
  workflowRunId={RUN_ID}
1017
1069
  />
1018
1070
  ),
@@ -1023,7 +1075,7 @@ describe('WorkflowRunView', () => {
1023
1075
 
1024
1076
  await waitFor(() =>
1025
1077
  expect(router.state.location.pathname).toBe(
1026
- `/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${RUN_ID}`,
1078
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}`,
1027
1079
  ),
1028
1080
  );
1029
1081
  expect(router.state.location.search).toEqual({runAttempt: 2});
@@ -1086,17 +1138,15 @@ describe('WorkflowRunView', () => {
1086
1138
  });
1087
1139
 
1088
1140
  function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {}, search = '') {
1089
- return renderProjectPage(
1090
- `/workspaces/${PROJECT_TEST_WID}/projects/x/runs/${RUN_ID}${search}`,
1091
- () => (
1092
- <WorkflowRunView
1093
- workspaceId={PROJECT_TEST_WID}
1094
- projectId={PROJECT_ID}
1095
- workflowRunId={RUN_ID}
1096
- {...props}
1097
- />
1098
- ),
1099
- );
1141
+ return renderProjectPage(`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}${search}`, () => (
1142
+ <WorkflowRunView
1143
+ projectId={PROJECT_ID}
1144
+ workspaceSlug={PROJECT_TEST_WSLUG}
1145
+ projectSlug="project"
1146
+ workflowRunId={RUN_ID}
1147
+ {...props}
1148
+ />
1149
+ ));
1100
1150
  }
1101
1151
 
1102
1152
  function requestUrl(input: RequestInfo | URL): URL {
@@ -1207,6 +1257,14 @@ function workflowRunViewDetailDto(
1207
1257
  status: 'running',
1208
1258
  position: 1,
1209
1259
  dependencies: ['build'],
1260
+ job_executions: [
1261
+ workflowJobExecutionDto({
1262
+ job_id: DEPLOY_JOB_ID,
1263
+ name: 'deploy',
1264
+ status: 'running',
1265
+ steps: [workflowStepDto({status: 'running'})],
1266
+ }),
1267
+ ],
1210
1268
  }),
1211
1269
  ],
1212
1270
  ...overrides,
@@ -36,8 +36,9 @@ interface WorkflowSourceFocus {
36
36
  }
37
37
 
38
38
  export interface WorkflowRunViewProps {
39
- workspaceId: string;
40
39
  projectId: string;
40
+ workspaceSlug?: string | undefined;
41
+ projectSlug?: string | undefined;
41
42
  workflowRunId?: string | undefined;
42
43
  selection?: WorkflowRunSelectionInput | undefined;
43
44
  onSelectionChange?: ((selection: WorkflowRunSelectionInput) => void) | undefined;
@@ -49,8 +50,9 @@ export interface WorkflowRunViewProps {
49
50
  * loading state itself.
50
51
  */
51
52
  export function WorkflowRunView({
52
- workspaceId,
53
53
  projectId,
54
+ workspaceSlug,
55
+ projectSlug,
54
56
  workflowRunId,
55
57
  selection,
56
58
  onSelectionChange,
@@ -62,8 +64,8 @@ export function WorkflowRunView({
62
64
  <RelativeTimeProvider>
63
65
  <div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
64
66
  <RunViewContent
65
- workspaceId={workspaceId}
66
- projectId={projectId}
67
+ workspaceSlug={workspaceSlug}
68
+ projectSlug={projectSlug}
67
69
  query={runQuery}
68
70
  rerunMutation={rerunMutation}
69
71
  selection={selection}
@@ -75,15 +77,15 @@ export function WorkflowRunView({
75
77
  }
76
78
 
77
79
  function RunViewContent({
78
- workspaceId,
79
- projectId,
80
+ workspaceSlug,
81
+ projectSlug,
80
82
  query,
81
83
  rerunMutation,
82
84
  selection,
83
85
  onSelectionChange,
84
86
  }: {
85
- workspaceId: string;
86
- projectId: string;
87
+ workspaceSlug: string | undefined;
88
+ projectSlug: string | undefined;
87
89
  query: ReturnType<typeof useWorkflowRunAttemptQuery>;
88
90
  rerunMutation: ReturnType<typeof useRerunWorkflowRunMutation>;
89
91
  selection: WorkflowRunSelectionInput | undefined;
@@ -161,12 +163,20 @@ function RunViewContent({
161
163
  sourceFocus?.location ?? resolvedSelection?.step?.sourceLocation ?? null;
162
164
  const sourceSnapshot = runData.sourceSnapshot;
163
165
  async function rerun(mode: WorkflowRunRerunMode) {
166
+ if (!workspaceSlug || !projectSlug) {
167
+ toast.error('Could not start re-run from this route.');
168
+ return;
169
+ }
164
170
  try {
165
171
  const run = await rerunMutation.mutateAsync({workflowRunId: runData.id, mode});
166
172
  toast.success('Re-run started');
167
173
  await navigate({
168
- to: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
169
- params: {wid: workspaceId, pid: projectId, workflowRunId: run.id},
174
+ to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
175
+ params: {
176
+ workspaceSlug,
177
+ projectSlug,
178
+ workflowRunId: run.id,
179
+ },
170
180
  search: ((previous: Record<string, unknown>) =>
171
181
  withoutWorkflowRunSelectionSearch(previous)) as never,
172
182
  });
@@ -274,8 +284,8 @@ function RunViewContent({
274
284
  <>
275
285
  <div className="flex min-w-0 flex-1 flex-col">
276
286
  <WorkflowRunSummary
277
- workspaceId={workspaceId}
278
- projectId={projectId}
287
+ workspaceSlug={workspaceSlug}
288
+ projectSlug={projectSlug}
279
289
  run={runData}
280
290
  sourceAvailable={sourceAvailable}
281
291
  sourceOpen={sourcePanelOpen && sourceFocus === null}
@@ -290,7 +300,7 @@ function RunViewContent({
290
300
  />
291
301
  {query.isError ? <WorkflowRunStaleError query={query} /> : null}
292
302
  <div className="min-h-0 flex-1 overflow-auto bg-background-neutral-base p-16">
293
- <div className="mx-auto flex w-full max-w-[1280px] flex-col gap-16">
303
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16">
294
304
  <JobGraph
295
305
  run={runData}
296
306
  selectedJobId={selectedJob?.id}
@@ -298,7 +308,7 @@ function RunViewContent({
298
308
  />
299
309
  {selectedJob ? (
300
310
  <JobCard
301
- workspaceId={workspaceId}
311
+ workspaceSlug={workspaceSlug}
302
312
  job={selectedJob}
303
313
  selectedJobExecution={selectedJobExecution}
304
314
  selectedAttemptId={selectedJob.carriedOver ? undefined : selectedAttemptId}
@@ -35,4 +35,13 @@ describe('getWorkflowStatusVisual', () => {
35
35
 
36
36
  expect(visual).toEqual({kind: 'running', label: 'Running', dot: 'info', badge: 'info'});
37
37
  });
38
+
39
+ test('returns the active listener visual', () => {
40
+ expect(getWorkflowStatusVisual('listening')).toEqual({
41
+ kind: 'listening',
42
+ label: 'Listening',
43
+ dot: 'info',
44
+ badge: 'info',
45
+ });
46
+ });
38
47
  });
@@ -1,11 +1,11 @@
1
1
  import type {BadgeVariant} from '@shipfox/react-ui/badge';
2
2
  import type {DotVariant} from '@shipfox/react-ui/dot';
3
- import type {WorkflowStatus} from '#core/workflow-run.js';
3
+ import type {WorkflowDisplayStatus} from '#core/workflow-run.js';
4
4
 
5
- export type {WorkflowStatus};
5
+ export type {WorkflowDisplayStatus};
6
6
 
7
7
  export interface WorkflowStatusVisual {
8
- kind: WorkflowStatus;
8
+ kind: WorkflowDisplayStatus;
9
9
  label: string;
10
10
  dot: DotVariant;
11
11
  badge: BadgeVariant;
@@ -15,12 +15,14 @@ export interface WorkflowStatusVisual {
15
15
  // WorkflowStatusIcon (which renders the glyph per kind). The exhaustive switch turns any new
16
16
  // status the API grows into (DESIGN.md section 9) into a compile error; reserve the `warning`
17
17
  // tone for the queued/awaiting-* states when they land.
18
- export function getWorkflowStatusVisual(status: WorkflowStatus): WorkflowStatusVisual {
18
+ export function getWorkflowStatusVisual(status: WorkflowDisplayStatus): WorkflowStatusVisual {
19
19
  switch (status) {
20
20
  case 'pending':
21
21
  return {kind: 'pending', label: 'Pending', dot: 'neutral', badge: 'neutral'};
22
22
  case 'running':
23
23
  return {kind: 'running', label: 'Running', dot: 'info', badge: 'info'};
24
+ case 'listening':
25
+ return {kind: 'listening', label: 'Listening', dot: 'info', badge: 'info'};
24
26
  case 'succeeded':
25
27
  return {kind: 'succeeded', label: 'Succeeded', dot: 'success', badge: 'success'};
26
28
  case 'failed':
@@ -1,10 +1,18 @@
1
1
  import {Code, Text} from '@shipfox/react-ui/typography';
2
2
  import type {Meta, StoryObj} from '@storybook/react';
3
- import type {JobStatus} from '#core/workflow-run.js';
3
+ import type {WorkflowDisplayStatus} from '#core/workflow-run.js';
4
4
  import {getWorkflowStatusVisual} from './status-visuals.js';
5
5
  import {WorkflowStatusIcon} from './workflow-status-icon.js';
6
6
 
7
- const statuses: JobStatus[] = ['pending', 'running', 'succeeded', 'failed', 'cancelled', 'skipped'];
7
+ const statuses: WorkflowDisplayStatus[] = [
8
+ 'pending',
9
+ 'running',
10
+ 'listening',
11
+ 'succeeded',
12
+ 'failed',
13
+ 'cancelled',
14
+ 'skipped',
15
+ ];
8
16
 
9
17
  const meta = {
10
18
  title: 'Workflows/StatusIcon',
@@ -0,0 +1,18 @@
1
+ import {render, screen} from '@testing-library/react';
2
+ import {WorkflowStatusIcon} from './workflow-status-icon.js';
3
+
4
+ describe('WorkflowStatusIcon', () => {
5
+ test('keeps a running status as a dot', () => {
6
+ const {container} = render(<WorkflowStatusIcon status="running" tooltip={false} />);
7
+
8
+ expect(screen.getByRole('img', {name: 'Running'})).toBeInTheDocument();
9
+ expect(container.querySelector('svg')).toBeNull();
10
+ });
11
+
12
+ test('uses the pulse glyph for the listening status', () => {
13
+ const {container} = render(<WorkflowStatusIcon status="listening" tooltip={false} />);
14
+
15
+ expect(screen.getByRole('img', {name: 'Listening'})).toBeInTheDocument();
16
+ expect(container.querySelector('svg')).not.toBeNull();
17
+ });
18
+ });
@@ -3,7 +3,7 @@ import {Icon, type IconName} from '@shipfox/react-ui/icon';
3
3
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
4
4
  import {cn} from '@shipfox/react-ui/utils';
5
5
  import type {ReactNode} from 'react';
6
- import {getWorkflowStatusVisual, type WorkflowStatus} from './status-visuals.js';
6
+ import {getWorkflowStatusVisual, type WorkflowDisplayStatus} from './status-visuals.js';
7
7
 
8
8
  // Status glyphs use the same saturated accent tokens as IconBadge/StatusBadge.
9
9
  const toneByVariant: Record<DotVariant, string> = {
@@ -17,7 +17,7 @@ const toneByVariant: Record<DotVariant, string> = {
17
17
 
18
18
  // Terminal states are self-contained solid discs; the shape names the state. Running and
19
19
  // pending render their own shapes (the live Dot and a bold ring), so they aren't here.
20
- const glyphByKind: Partial<Record<WorkflowStatus, IconName>> = {
20
+ const glyphByKind: Partial<Record<WorkflowDisplayStatus, IconName>> = {
21
21
  succeeded: 'checkCircleSolid',
22
22
  failed: 'xCircleSolid',
23
23
  cancelled: 'forbid2Fill',
@@ -45,8 +45,7 @@ const dotSizeClass: Record<number, string> = {
45
45
  const PENDING_RING_MASK = 'radial-gradient(circle closest-side, transparent 0 52%, #000 56%)';
46
46
 
47
47
  export interface WorkflowStatusIconProps {
48
- status: WorkflowStatus;
49
- jobMode?: 'one_shot' | 'listening';
48
+ status: WorkflowDisplayStatus;
50
49
  /** Optical diameter in px: 14 in the DAG node and run row. */
51
50
  size?: number;
52
51
  /** Pulsing halo for the running state. */
@@ -66,7 +65,6 @@ export interface WorkflowStatusIconProps {
66
65
  */
67
66
  export function WorkflowStatusIcon({
68
67
  status,
69
- jobMode = 'one_shot',
70
68
  size = 14,
71
69
  ripple = true,
72
70
  tooltip = true,
@@ -76,25 +74,20 @@ export function WorkflowStatusIcon({
76
74
  const box = dotSizeClass[size] ?? 'size-14';
77
75
 
78
76
  let glyph: ReactNode;
79
- if (visual.kind === 'running') {
80
- glyph =
81
- jobMode === 'listening' ? (
82
- <span
83
- className={cn(
84
- 'inline-flex shrink-0 items-center justify-center rounded-full bg-current',
85
- box,
86
- toneByVariant.info,
87
- )}
88
- >
89
- <Icon
90
- name="pulseLine"
91
- size={Math.max(8, Math.round(size * 0.7))}
92
- className="text-white"
93
- />
94
- </span>
95
- ) : (
96
- <Dot variant="info" ripple={ripple} className={box} />
97
- );
77
+ if (visual.kind === 'listening') {
78
+ glyph = (
79
+ <span
80
+ className={cn(
81
+ 'inline-flex shrink-0 items-center justify-center rounded-full bg-current',
82
+ box,
83
+ toneByVariant.info,
84
+ )}
85
+ >
86
+ <Icon name="pulseLine" size={Math.max(8, Math.round(size * 0.7))} className="text-white" />
87
+ </span>
88
+ );
89
+ } else if (visual.kind === 'running') {
90
+ glyph = <Dot variant="info" ripple={ripple} className={box} />;
98
91
  } else if (visual.kind === 'pending') {
99
92
  glyph = (
100
93
  <span
@@ -2,11 +2,16 @@ import {type Duration, intervalToDuration} from 'date-fns';
2
2
  import type {Step} from './step.js';
3
3
 
4
4
  export type JobExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
5
+ export type JobExecutionDisplayStatus = JobExecutionStatus;
5
6
  export interface WorkflowExecutionEvent {
6
7
  source: string;
7
8
  event: string;
8
9
  deliveryId: string;
9
10
  receivedAt: string;
11
+ project: {id: string} | null;
12
+ repository: string | null;
13
+ ref: string | null;
14
+ commit: string | null;
10
15
  data: unknown;
11
16
  }
12
17
  export type JobExecutionTime =
@@ -53,6 +58,10 @@ export class JobExecution {
53
58
  Object.assign(this, fields);
54
59
  }
55
60
 
61
+ get displayName(): string {
62
+ return this.name;
63
+ }
64
+
56
65
  get queueTime(): JobExecutionTime | null {
57
66
  return jobExecutionQueueTimeFromTimestamps(this);
58
67
  }
@@ -66,6 +75,19 @@ export class JobExecution {
66
75
  }
67
76
  }
68
77
 
78
+ export function deriveJobExecutionDisplayStatus({
79
+ status,
80
+ steps,
81
+ }: Pick<JobExecution, 'status' | 'steps'>): JobExecutionDisplayStatus {
82
+ if (isTerminalJobExecutionStatus(status)) return status;
83
+ if (steps.some((step) => step.status === 'running')) return 'running';
84
+ return 'pending';
85
+ }
86
+
87
+ export function isTerminalJobExecutionStatus(status: JobExecutionStatus): boolean {
88
+ return status === 'succeeded' || status === 'failed' || status === 'cancelled';
89
+ }
90
+
69
91
  export function jobExecutionQueueTimeFromTimestamps({
70
92
  queuedAt,
71
93
  startedAt,