@shipfox/client-workflows 12.0.1 → 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 +65 -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
@@ -0,0 +1,104 @@
1
+ import {deriveJobDisplayStatus, type JobDisplayStatus} from './job.js';
2
+ import {deriveJobExecutionDisplayStatus, type JobExecutionStatus} from './job-execution.js';
3
+
4
+ type DisplayStep = Parameters<typeof deriveJobExecutionDisplayStatus>[0]['steps'][number];
5
+
6
+ function displayStep(status: string): DisplayStep {
7
+ return {status} as DisplayStep;
8
+ }
9
+
10
+ const EXECUTION_STATUSES = ['pending', 'running', 'succeeded', 'failed', 'cancelled'] as const;
11
+ const TERMINAL_EXECUTION_STATUSES = ['succeeded', 'failed', 'cancelled'] as const;
12
+
13
+ describe('deriveJobExecutionDisplayStatus', () => {
14
+ test.each(
15
+ EXECUTION_STATUSES,
16
+ )('%s without a running step displays Pending unless terminal', (status) => {
17
+ const displayStatus = deriveJobExecutionDisplayStatus({status, steps: []});
18
+
19
+ expect(displayStatus).toBe(
20
+ TERMINAL_EXECUTION_STATUSES.includes(status as (typeof TERMINAL_EXECUTION_STATUSES)[number])
21
+ ? status
22
+ : 'pending',
23
+ );
24
+ });
25
+
26
+ test.each([
27
+ 'pending',
28
+ 'running',
29
+ ] as const)('%s with a running step displays Running', (status) => {
30
+ expect(deriveJobExecutionDisplayStatus({status, steps: [displayStep('running')]})).toBe(
31
+ 'running',
32
+ );
33
+ });
34
+
35
+ test.each(TERMINAL_EXECUTION_STATUSES)('%s wins over a running child step', (status) => {
36
+ expect(deriveJobExecutionDisplayStatus({status, steps: [displayStep('running')]})).toBe(status);
37
+ });
38
+ });
39
+
40
+ describe('deriveJobDisplayStatus', () => {
41
+ test('uses the execution activity for a one-shot job', () => {
42
+ const statuses: Array<[JobExecutionStatus, string[], JobDisplayStatus]> = [
43
+ ['pending', [], 'pending'],
44
+ ['pending', ['running'], 'running'],
45
+ ['running', [], 'pending'],
46
+ ['running', ['running'], 'running'],
47
+ ];
48
+
49
+ for (const [status, stepStatuses, expected] of statuses) {
50
+ expect(
51
+ deriveJobDisplayStatus({
52
+ mode: 'one_shot',
53
+ status: 'running',
54
+ listenerStatus: 'inactive',
55
+ jobExecutions: [
56
+ {
57
+ status,
58
+ steps: stepStatuses.map(displayStep),
59
+ sequence: 1,
60
+ },
61
+ ] as never,
62
+ }),
63
+ ).toBe(expected);
64
+ }
65
+ });
66
+
67
+ test.each([
68
+ 'succeeded',
69
+ 'failed',
70
+ 'cancelled',
71
+ 'skipped',
72
+ ] as const)('terminal job status %s wins over child activity', (status) => {
73
+ expect(
74
+ deriveJobDisplayStatus({
75
+ mode: 'one_shot',
76
+ status,
77
+ listenerStatus: 'inactive',
78
+ jobExecutions: [{status: 'running', steps: [{status: 'running'}], sequence: 1}] as never,
79
+ }),
80
+ ).toBe(status);
81
+ });
82
+
83
+ test('keeps an active listener as Listening without executions', () => {
84
+ expect(
85
+ deriveJobDisplayStatus({
86
+ mode: 'listening',
87
+ status: 'running',
88
+ listenerStatus: 'listening',
89
+ jobExecutions: [],
90
+ }),
91
+ ).toBe('listening');
92
+ });
93
+
94
+ test('uses the terminal job status when an active listener has resolved', () => {
95
+ expect(
96
+ deriveJobDisplayStatus({
97
+ mode: 'listening',
98
+ status: 'succeeded',
99
+ listenerStatus: 'listening',
100
+ jobExecutions: [],
101
+ }),
102
+ ).toBe('succeeded');
103
+ });
104
+ });
@@ -1,6 +1,11 @@
1
- import type {JobExecution, JobExecutionDisplayDuration} from './job-execution.js';
1
+ import {
2
+ deriveJobExecutionDisplayStatus,
3
+ type JobExecution,
4
+ type JobExecutionDisplayDuration,
5
+ } from './job-execution.js';
2
6
 
3
7
  export type JobStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'skipped';
8
+ export type JobDisplayStatus = JobStatus | 'listening';
4
9
  export type JobMode = 'one_shot' | 'listening';
5
10
  export type ListenerStatus = 'inactive' | 'listening' | 'resolved';
6
11
  export type ResolutionReason = 'until' | 'timeout' | 'max_executions' | 'cancelled';
@@ -103,7 +108,7 @@ export class Job {
103
108
  }
104
109
 
105
110
  get displayName(): string {
106
- return this.name || this.key;
111
+ return this.name ?? this.key;
107
112
  }
108
113
 
109
114
  get displayDuration(): JobDisplayDuration | null {
@@ -123,6 +128,16 @@ export function isTerminalJobStatus(status: JobStatus): boolean {
123
128
  return TERMINAL_JOB_STATUS_SET.has(status);
124
129
  }
125
130
 
131
+ export function deriveJobDisplayStatus(
132
+ job: Pick<Job, 'mode' | 'status' | 'listenerStatus' | 'jobExecutions'>,
133
+ ): JobDisplayStatus {
134
+ if (isTerminalJobStatus(job.status)) return job.status;
135
+ if (job.mode === 'listening' && job.listenerStatus === 'listening') return 'listening';
136
+
137
+ const execution = defaultJobExecution(job);
138
+ return execution ? deriveJobExecutionDisplayStatus(execution) : 'pending';
139
+ }
140
+
126
141
  export function resolveJobExecution(
127
142
  job: Job,
128
143
  jobExecutionId: string | undefined,
@@ -135,7 +150,7 @@ export function resolveJobExecution(
135
150
  return defaultJobExecution(job);
136
151
  }
137
152
 
138
- export function defaultJobExecution(job: Job): JobExecution | undefined {
153
+ export function defaultJobExecution(job: Pick<Job, 'jobExecutions'>): JobExecution | undefined {
139
154
  const runningExecution = job.jobExecutions.find(
140
155
  (jobExecution) => jobExecution.status === 'running',
141
156
  );
@@ -1,9 +1,10 @@
1
- import {type Job, WORKFLOW_JOB_STATUSES} from './job.js';
1
+ import {type Job, type JobStatus, WORKFLOW_JOB_STATUSES} from './job.js';
2
2
  import type {WorkflowRunAttempt, WorkflowRunAttemptSummary} from './workflow-run-attempt.js';
3
3
 
4
4
  export type WorkflowRunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
5
5
  export type WorkflowRunRerunMode = 'all' | 'failed';
6
6
  export type WorkflowStatus = WorkflowRunStatus | (typeof WORKFLOW_JOB_STATUSES)[number];
7
+ export type WorkflowDisplayStatus = WorkflowStatus | 'listening';
7
8
 
8
9
  export const WORKFLOW_RUN_STATUSES = [
9
10
  'pending',
@@ -30,11 +31,51 @@ export interface WorkflowSourceSnapshot {
30
31
  format: 'yaml';
31
32
  }
32
33
 
34
+ /**
35
+ * Provider-neutral trigger facts. Every field is nullable: only source-control triggers
36
+ * resolve a reference at all, and a payload can name a ref without naming an actor.
37
+ */
38
+ export interface WorkflowRunTriggerReference {
39
+ repository: string | null;
40
+ ref: string | null;
41
+ commit: string | null;
42
+ actor: string | null;
43
+ }
44
+
45
+ /** One glyph in a run row's job status strip: enough to draw and label it, nothing more. */
46
+ export interface WorkflowRunJobSummary {
47
+ id: string;
48
+ key: string;
49
+ name: string | null;
50
+ status: JobStatus;
51
+ position: number;
52
+ }
53
+
54
+ export interface WorkflowRunJobStatusCount {
55
+ status: JobStatus;
56
+ count: number;
57
+ }
58
+
59
+ /**
60
+ * A run's jobs as the list receives them: a bounded slice to draw, plus totals covering all
61
+ * of them.
62
+ *
63
+ * `preview` is capped by the API, so `preview.length` is not the job count and the strip
64
+ * reads `total` and `statusCounts` for anything it says rather than anything it draws.
65
+ */
66
+ export interface WorkflowRunJobs {
67
+ preview: WorkflowRunJobSummary[];
68
+ statusCounts: WorkflowRunJobStatusCount[];
69
+ total: number;
70
+ }
71
+
33
72
  export interface WorkflowRun {
34
73
  id: string;
35
74
  projectId: string;
36
75
  definitionId: string;
76
+ number: number | null;
37
77
  name: string;
78
+ workflowName: string;
38
79
  currentAttempt: number;
39
80
  triggerProvider: string | null;
40
81
  triggerSource: string;
@@ -42,20 +83,25 @@ export interface WorkflowRun {
42
83
  triggerDisplayLabel: string;
43
84
  triggerLabel: string;
44
85
  triggerPayload: Record<string, unknown>;
86
+ triggerReference: WorkflowRunTriggerReference | null;
45
87
  inputs: Record<string, unknown> | null;
46
88
  sourceSnapshot: WorkflowSourceSnapshot | null;
47
89
  createdAt: string;
48
90
  updatedAt: string;
49
- shortId: string;
50
91
  isTemporary: boolean;
51
92
  }
52
93
 
53
- export interface WorkflowRunListItem extends WorkflowRun {
94
+ /** A run as the API returns it outside the list: no job strip, because none was fetched. */
95
+ export interface WorkflowRunRecord extends WorkflowRun {
54
96
  status: WorkflowRunStatus;
55
97
  latestAttempt: number;
56
98
  runAttempt: WorkflowRunAttemptSummary;
57
99
  }
58
100
 
101
+ export interface WorkflowRunListItem extends WorkflowRunRecord {
102
+ jobs: WorkflowRunJobs;
103
+ }
104
+
59
105
  export interface WorkflowRunDetail extends WorkflowRun {
60
106
  latestAttempt: number;
61
107
  runAttempt: WorkflowRunAttempt;
@@ -72,10 +118,6 @@ export interface ManualWorkflowLaunch {
72
118
  workflowRunId: string;
73
119
  }
74
120
 
75
- export function workflowRunShortId(id: string): string {
76
- return id.length <= 8 ? id : id.slice(0, 8);
77
- }
78
-
79
121
  export function workflowRunTriggerLabel({
80
122
  triggerSource,
81
123
  triggerEvent,
@@ -96,6 +138,33 @@ export function workflowRunTriggerDisplayLabel({
96
138
  return triggerEvent || triggerSource;
97
139
  }
98
140
 
141
+ const SHORT_COMMIT_LENGTH = 7;
142
+ const PULL_REQUEST_REF_PATTERN = /^refs\/pull\/(\d+)\/head$/u;
143
+
144
+ /**
145
+ * The ref as a person names it: `main` for a branch, `#42` for a pull request, the tag for a
146
+ * tag. An unrecognized ref is shown verbatim rather than hidden, since a ref nobody can read
147
+ * is still better evidence than a blank cell.
148
+ */
149
+ export function workflowRunBranchLabel(run: Pick<WorkflowRun, 'triggerReference'>): string | null {
150
+ const ref = run.triggerReference?.ref;
151
+ if (!ref) return null;
152
+ const pullRequest = PULL_REQUEST_REF_PATTERN.exec(ref);
153
+ if (pullRequest) return `#${pullRequest[1]}`;
154
+ if (ref.startsWith('refs/heads/')) return ref.slice('refs/heads/'.length);
155
+ if (ref.startsWith('refs/tags/')) return ref.slice('refs/tags/'.length);
156
+ return ref;
157
+ }
158
+
159
+ export function workflowRunCommitLabel(run: Pick<WorkflowRun, 'triggerReference'>): string | null {
160
+ const commit = run.triggerReference?.commit;
161
+ return commit ? commit.slice(0, SHORT_COMMIT_LENGTH) : null;
162
+ }
163
+
164
+ export function workflowRunActor(run: Pick<WorkflowRun, 'triggerReference'>): string | null {
165
+ return run.triggerReference?.actor ?? null;
166
+ }
167
+
99
168
  export function isWorkflowRunTerminal(status: WorkflowRunStatus): boolean {
100
169
  return TERMINAL_WORKFLOW_RUN_STATUS_SET.has(status);
101
170
  }
@@ -6,6 +6,7 @@ import {
6
6
  toWorkflowRunListPage,
7
7
  } from '#hooks/api/workflow-run-mapper.js';
8
8
  import {
9
+ workflowJob,
9
10
  workflowJobDto,
10
11
  workflowJobExecutionDto,
11
12
  workflowRunAttemptDto,
@@ -18,7 +19,6 @@ import {
18
19
  import {
19
20
  isWorkflowRunTerminal,
20
21
  isWorkflowStatus,
21
- workflowRunShortId,
22
22
  workflowRunTriggerDisplayLabel,
23
23
  workflowRunTriggerLabel,
24
24
  } from './workflow-run.js';
@@ -51,7 +51,9 @@ describe('workflow run model mapping', () => {
51
51
  id: '66666666-6666-4666-8666-666666666666',
52
52
  projectId: '44444444-4444-4444-8444-444444444444',
53
53
  definitionId: '55555555-5555-4555-8555-555555555555',
54
+ number: 1,
54
55
  name: 'deploy-web',
56
+ workflowName: 'deploy-web',
55
57
  currentAttempt: 3,
56
58
  triggerProvider: 'github',
57
59
  triggerSource: 'github_acme',
@@ -63,7 +65,6 @@ describe('workflow run model mapping', () => {
63
65
  sourceSnapshot: {format: 'yaml', content: 'jobs: {}'},
64
66
  createdAt: '2026-05-07T01:01:00.000Z',
65
67
  updatedAt: '2026-05-07T01:02:00.000Z',
66
- shortId: '66666666',
67
68
  isTemporary: false,
68
69
  });
69
70
  expect(run).not.toHaveProperty('status');
@@ -91,7 +92,6 @@ describe('workflow run model mapping', () => {
91
92
  triggerLabel: '',
92
93
  inputs: null,
93
94
  sourceSnapshot: null,
94
- shortId: 'temp-123',
95
95
  isTemporary: true,
96
96
  });
97
97
  });
@@ -299,6 +299,10 @@ describe('workflow run model mapping', () => {
299
299
  event: 'deployment_status',
300
300
  delivery_id: 'delivery-1',
301
301
  received_at: '2026-05-07T01:00:00.000Z',
302
+ project: {id: '44444444-4444-4444-8444-444444444444'},
303
+ repository: 'acme/api',
304
+ ref: 'refs/heads/main',
305
+ commit: 'a'.repeat(40),
302
306
  data: {state: 'success'},
303
307
  },
304
308
  ],
@@ -332,6 +336,10 @@ describe('workflow run model mapping', () => {
332
336
  event: 'deployment_status',
333
337
  deliveryId: 'delivery-1',
334
338
  receivedAt: '2026-05-07T01:00:00.000Z',
339
+ project: {id: '44444444-4444-4444-8444-444444444444'},
340
+ repository: 'acme/api',
341
+ ref: 'refs/heads/main',
342
+ commit: 'a'.repeat(40),
335
343
  data: {state: 'success'},
336
344
  },
337
345
  ],
@@ -376,6 +384,18 @@ describe('workflow run model mapping', () => {
376
384
  });
377
385
  });
378
386
 
387
+ test('exposes display names for jobs and job executions', () => {
388
+ const job = workflowJob({
389
+ name: 'Deploy',
390
+ key: 'deploy',
391
+ job_executions: [workflowJobExecutionDto({name: 'Deploy production'})],
392
+ });
393
+
394
+ expect(job.displayName).toBe('Deploy');
395
+ expect(job.jobExecutions[0]?.displayName).toBe('Deploy production');
396
+ expect(workflowJob({name: null, key: 'deploy-prod'}).displayName).toBe('deploy-prod');
397
+ });
398
+
379
399
  test('returns no job display duration when a job has multiple executions', () => {
380
400
  const job = workflowJobDto({
381
401
  job_executions: [
@@ -628,12 +648,7 @@ describe('workflow run helpers', () => {
628
648
  expect(neither).toBe('');
629
649
  });
630
650
 
631
- test('formats short ids and classifies workflow statuses', () => {
632
- const short = workflowRunShortId('abc123');
633
- const long = workflowRunShortId('66666666-6666-4666-8666-666666666666');
634
-
635
- expect(short).toBe('abc123');
636
- expect(long).toBe('66666666');
651
+ test('classifies workflow statuses', () => {
637
652
  expect(isWorkflowRunTerminal('succeeded')).toBe(true);
638
653
  expect(isWorkflowRunTerminal('running')).toBe(false);
639
654
  expect(isWorkflowStatus('pending')).toBe(true);
@@ -1,5 +1,6 @@
1
1
  export type {
2
2
  JobDisplayDuration,
3
+ JobDisplayStatus,
3
4
  JobListening,
4
5
  JobMode,
5
6
  JobStatus,
@@ -9,6 +10,7 @@ export type {
9
10
  } from './entities/job.js';
10
11
  export {
11
12
  defaultJobExecution,
13
+ deriveJobDisplayStatus,
12
14
  isTerminalJobStatus,
13
15
  Job,
14
16
  resolveJobExecution,
@@ -17,11 +19,16 @@ export {
17
19
  } from './entities/job.js';
18
20
  export type {
19
21
  JobExecutionDisplayDuration,
22
+ JobExecutionDisplayStatus,
20
23
  JobExecutionStatus,
21
24
  JobExecutionTime,
22
25
  WorkflowExecutionEvent,
23
26
  } from './entities/job-execution.js';
24
- export {JobExecution} from './entities/job-execution.js';
27
+ export {
28
+ deriveJobExecutionDisplayStatus,
29
+ isTerminalJobExecutionStatus,
30
+ JobExecution,
31
+ } from './entities/job-execution.js';
25
32
  export type {
26
33
  AgentConfigIssue,
27
34
  AgentStepConfig,
@@ -39,12 +46,18 @@ export type {
39
46
  export {StepAttempt} from './entities/step-attempt.js';
40
47
  export type {
41
48
  ManualWorkflowLaunch,
49
+ WorkflowDisplayStatus,
42
50
  WorkflowRun,
43
51
  WorkflowRunDetail,
52
+ WorkflowRunJobStatusCount,
53
+ WorkflowRunJobSummary,
54
+ WorkflowRunJobs,
44
55
  WorkflowRunListItem,
45
56
  WorkflowRunListPage,
57
+ WorkflowRunRecord,
46
58
  WorkflowRunRerunMode,
47
59
  WorkflowRunStatus,
60
+ WorkflowRunTriggerReference,
48
61
  WorkflowSourceSnapshot,
49
62
  WorkflowStatus,
50
63
  } from './entities/workflow-run.js';
@@ -53,7 +66,9 @@ export {
53
66
  isWorkflowStatus,
54
67
  TERMINAL_WORKFLOW_RUN_STATUSES,
55
68
  WORKFLOW_RUN_STATUSES,
56
- workflowRunShortId,
69
+ workflowRunActor,
70
+ workflowRunBranchLabel,
71
+ workflowRunCommitLabel,
57
72
  workflowRunTriggerDisplayLabel,
58
73
  workflowRunTriggerLabel,
59
74
  } from './entities/workflow-run.js';
package/src/feature.ts CHANGED
@@ -5,14 +5,14 @@ export const workflowsNavigation = [
5
5
  id: 'nav.runs',
6
6
  scope: 'project',
7
7
  label: 'Runs',
8
- to: '/workspaces/$wid/projects/$pid/runs',
8
+ to: '/w/$workspaceSlug/p/$projectSlug/runs',
9
9
  order: 100,
10
10
  },
11
11
  {
12
12
  id: 'nav.workflows',
13
13
  scope: 'project',
14
14
  label: 'Workflows',
15
- to: '/workspaces/$wid/projects/$pid/workflows',
15
+ to: '/w/$workspaceSlug/p/$projectSlug/workflows',
16
16
  order: 200,
17
17
  },
18
18
  ] as const satisfies readonly NavTabEntry[];
@@ -21,17 +21,17 @@ export const workflowsFeature = defineClientFeature({
21
21
  id: 'shipfox.workflows',
22
22
  routes: [
23
23
  {
24
- path: '/workspaces/$wid/projects/$pid/workflows',
24
+ path: '/w/$workspaceSlug/p/$projectSlug/workflows',
25
25
  parent: 'projectLayout',
26
26
  impl: '@shipfox/client-workflows/routes/workflows',
27
27
  },
28
28
  {
29
- path: '/workspaces/$wid/projects/$pid/runs',
29
+ path: '/w/$workspaceSlug/p/$projectSlug/runs',
30
30
  parent: 'projectLayout',
31
31
  impl: '@shipfox/client-workflows/routes/runs',
32
32
  },
33
33
  {
34
- path: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
34
+ path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
35
35
  parent: 'projectLayout',
36
36
  impl: '@shipfox/client-workflows/routes/run-detail',
37
37
  },
@@ -7,6 +7,7 @@ import type {
7
7
  WorkflowRunDetailResponseDto,
8
8
  WorkflowRunJobDetailDto,
9
9
  WorkflowRunJobExecutionDetailDto,
10
+ WorkflowRunListItemDto,
10
11
  WorkflowRunListResponseDto,
11
12
  WorkflowRunResponseDto,
12
13
  WorkflowRunStepDetailDto,
@@ -25,7 +26,7 @@ import {
25
26
  type WorkflowRunDetail,
26
27
  type WorkflowRunListItem,
27
28
  type WorkflowRunListPage,
28
- workflowRunShortId,
29
+ type WorkflowRunRecord,
29
30
  workflowRunTriggerDisplayLabel,
30
31
  workflowRunTriggerLabel,
31
32
  } from '#core/workflow-run.js';
@@ -35,7 +36,9 @@ export function toWorkflowRun(dto: WorkflowRunResponseDto): WorkflowRun {
35
36
  id: dto.id,
36
37
  projectId: dto.project_id,
37
38
  definitionId: dto.definition_id,
39
+ number: dto.number,
38
40
  name: dto.name,
41
+ workflowName: dto.workflow_name,
39
42
  currentAttempt: dto.current_attempt,
40
43
  triggerProvider: dto.trigger_provider,
41
44
  triggerSource: dto.trigger_source,
@@ -49,13 +52,13 @@ export function toWorkflowRun(dto: WorkflowRunResponseDto): WorkflowRun {
49
52
  triggerEvent: dto.trigger_event,
50
53
  }),
51
54
  triggerPayload: dto.trigger_payload,
55
+ triggerReference: dto.trigger_reference,
52
56
  inputs: dto.inputs ?? null,
53
57
  sourceSnapshot: dto.source_snapshot
54
58
  ? {content: dto.source_snapshot.content, format: dto.source_snapshot.format}
55
59
  : null,
56
60
  createdAt: dto.created_at,
57
61
  updatedAt: dto.updated_at,
58
- shortId: workflowRunShortId(dto.id),
59
62
  isTemporary: dto.id.startsWith('temp-'),
60
63
  };
61
64
  }
@@ -73,7 +76,7 @@ export function toWorkflowRunAttempt(dto: WorkflowRunAttemptDto): WorkflowRunAtt
73
76
  });
74
77
  }
75
78
 
76
- export function toWorkflowRunListItem(dto: WorkflowRunResponseDto): WorkflowRunListItem {
79
+ export function toWorkflowRunRecord(dto: WorkflowRunResponseDto): WorkflowRunRecord {
77
80
  return {
78
81
  ...toWorkflowRun(dto),
79
82
  status: dto.status,
@@ -89,6 +92,26 @@ export function toWorkflowRunListItem(dto: WorkflowRunResponseDto): WorkflowRunL
89
92
  };
90
93
  }
91
94
 
95
+ export function toWorkflowRunListItem(dto: WorkflowRunListItemDto): WorkflowRunListItem {
96
+ const statusCounts = dto.job_status_counts.map(({status, count}) => ({status, count}));
97
+ return {
98
+ ...toWorkflowRunRecord(dto),
99
+ jobs: {
100
+ preview: dto.jobs.map((job) => ({
101
+ id: job.id,
102
+ key: job.key,
103
+ name: job.name,
104
+ status: job.status,
105
+ position: job.position,
106
+ })),
107
+ statusCounts,
108
+ // Derived rather than sent: the counts already cover every job, and a separate total
109
+ // would be a second source of truth that could disagree with them.
110
+ total: statusCounts.reduce((sum, entry) => sum + entry.count, 0),
111
+ },
112
+ };
113
+ }
114
+
92
115
  export function toWorkflowRunListPage(dto: WorkflowRunListResponseDto): WorkflowRunListPage {
93
116
  return {
94
117
  runs: dto.runs.map(toWorkflowRunListItem),
@@ -220,6 +243,10 @@ function toWorkflowExecutionEvent(dto: WorkflowExecutionEventDto): WorkflowExecu
220
243
  event: dto.event,
221
244
  deliveryId: dto.delivery_id,
222
245
  receivedAt: dto.received_at,
246
+ project: dto.project,
247
+ repository: dto.repository,
248
+ ref: dto.ref,
249
+ commit: dto.commit,
223
250
  data: dto.data,
224
251
  };
225
252
  }
@@ -240,6 +240,8 @@ describe('workflow run API hooks', () => {
240
240
  expect(cached?.pages[0]?.runs[0]).toMatchObject({
241
241
  projectId: PROJECT_ID,
242
242
  definitionId: DEFINITION_ID,
243
+ number: null,
244
+ workflowName: 'New run',
243
245
  status: 'pending',
244
246
  triggerSource: 'manual',
245
247
  triggerProvider: null,
@@ -29,13 +29,14 @@ import {
29
29
  type WorkflowRunDetail,
30
30
  type WorkflowRunListItem,
31
31
  type WorkflowRunListPage,
32
+ type WorkflowRunRecord,
32
33
  type WorkflowRunStatus,
33
34
  } from '#core/workflow-run.js';
34
35
  import {
35
36
  toWorkflowRunAttempt,
36
37
  toWorkflowRunDetail,
37
- toWorkflowRunListItem,
38
38
  toWorkflowRunListPage,
39
+ toWorkflowRunRecord,
39
40
  } from './workflow-run-mapper.js';
40
41
 
41
42
  export interface WorkflowRunFilters {
@@ -250,8 +251,8 @@ async function cancelWorkflowRun({
250
251
  workflowRunId,
251
252
  }: {
252
253
  workflowRunId: string;
253
- }): Promise<WorkflowRunListItem> {
254
- return toWorkflowRunListItem(
254
+ }): Promise<WorkflowRunRecord> {
255
+ return toWorkflowRunRecord(
255
256
  await checkedApiRequest(workflowRunDtoSchema, `/workflows/runs/${workflowRunId}/cancel`, {
256
257
  method: 'POST',
257
258
  }),
@@ -264,8 +265,8 @@ export async function rerunWorkflowRun({
264
265
  }: {
265
266
  workflowRunId: string;
266
267
  mode: WorkflowRunRerunModeDto;
267
- }): Promise<WorkflowRunListItem> {
268
- return toWorkflowRunListItem(
268
+ }): Promise<WorkflowRunRecord> {
269
+ return toWorkflowRunRecord(
269
270
  await checkedApiRequest(workflowRunResponseSchema, `/workflows/runs/${workflowRunId}/rerun`, {
270
271
  method: 'POST',
271
272
  body: {mode} satisfies RerunWorkflowRunBodyDto,
@@ -338,7 +339,9 @@ function buildTempRun({
338
339
  id,
339
340
  projectId,
340
341
  definitionId,
342
+ number: null,
341
343
  name,
344
+ workflowName: name,
342
345
  status: 'pending',
343
346
  currentAttempt: 1,
344
347
  latestAttempt: 1,
@@ -348,12 +351,14 @@ function buildTempRun({
348
351
  triggerPayload: {source: 'manual', event: 'fire'},
349
352
  triggerDisplayLabel: 'fire',
350
353
  triggerLabel: 'manual · fire',
354
+ triggerReference: null,
351
355
  inputs: null,
352
356
  sourceSnapshot: null,
353
357
  createdAt,
354
358
  updatedAt: createdAt,
355
- shortId: id.slice(0, 8),
356
359
  isTemporary: true,
360
+ // The optimistic row genuinely has no jobs yet: the server has not planned the graph.
361
+ jobs: {preview: [], statusCounts: [], total: 0},
357
362
  runAttempt: new WorkflowRunAttemptSummary({
358
363
  workflowRunId: id,
359
364
  attempt: 1,
@@ -568,7 +573,7 @@ export function workflowRunAttemptsQueryOptions({
568
573
  export function useCancelWorkflowRunMutation(run: WorkflowRun | undefined) {
569
574
  const queryClient = useQueryClient();
570
575
  return useMutation({
571
- mutationFn: async (): Promise<WorkflowRunListItem> => {
576
+ mutationFn: async (): Promise<WorkflowRunRecord> => {
572
577
  if (!run) throw new Error('Workflow run is not loaded');
573
578
  return await cancelWorkflowRun({workflowRunId: run.id});
574
579
  },
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export type {
2
2
  AgentConfigIssue,
3
3
  JobDisplayDuration,
4
+ JobDisplayStatus,
4
5
  JobExecutionDisplayDuration,
6
+ JobExecutionDisplayStatus,
5
7
  JobExecutionStatus,
6
8
  JobExecutionTime,
7
9
  JobStatus,
@@ -14,6 +16,7 @@ export type {
14
16
  StepErrorReason,
15
17
  StepGateResult,
16
18
  StepSourceLocation,
19
+ WorkflowDisplayStatus,
17
20
  WorkflowRun,
18
21
  WorkflowRunAttempt,
19
22
  WorkflowRunDetail,
@@ -23,13 +26,14 @@ export type {
23
26
  WorkflowStatus,
24
27
  } from '#core/workflow-run.js';
25
28
  export {
29
+ deriveJobDisplayStatus,
30
+ deriveJobExecutionDisplayStatus,
26
31
  isWorkflowRunTerminal,
27
32
  isWorkflowStatus,
28
33
  Job,
29
34
  JobExecution,
30
35
  TERMINAL_WORKFLOW_RUN_STATUSES,
31
36
  WORKFLOW_RUN_STATUSES,
32
- workflowRunShortId,
33
37
  workflowRunTriggerLabel,
34
38
  } from '#core/workflow-run.js';
35
39
  export {
@@ -47,4 +51,5 @@ export {
47
51
  workflowRunsQueryKeys,
48
52
  } from './hooks/api/workflow-runs.js';
49
53
  export {ProjectWorkflowsPage} from './pages/project-workflows-page.js';
50
- export {WorkflowRunPage} from './pages/workflow-run-page.js';
54
+ export {WorkflowRunDetailPage} from './pages/workflow-run-detail-page.js';
55
+ export {WorkflowRunsPage} from './pages/workflow-run-list-page.js';