@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
@@ -14,6 +14,7 @@ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip
14
14
  import {Header, Text} from '@shipfox/react-ui/typography';
15
15
  import type {Ref} from 'react';
16
16
  import {useId} from 'react';
17
+ import {WorkflowRunNumberLabel} from '#components/workflow-run-number-label.js';
17
18
  import {
18
19
  isWorkflowRunTerminal,
19
20
  type Job,
@@ -32,8 +33,8 @@ const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
32
33
  type WorkflowRunAction = 'cancel' | 'rerun-all' | 'rerun-menu' | 'none';
33
34
 
34
35
  export interface WorkflowRunSummaryProps {
35
- workspaceId?: string | undefined;
36
- projectId?: string | undefined;
36
+ workspaceSlug?: string | undefined;
37
+ projectSlug?: string | undefined;
37
38
  run: WorkflowRunDetail;
38
39
  sourceAvailable?: boolean | undefined;
39
40
  sourceOpen?: boolean | undefined;
@@ -48,8 +49,8 @@ export interface WorkflowRunSummaryProps {
48
49
  }
49
50
 
50
51
  export function WorkflowRunSummary({
51
- workspaceId,
52
- projectId,
52
+ workspaceSlug,
53
+ projectSlug,
53
54
  run,
54
55
  sourceAvailable = false,
55
56
  sourceOpen = false,
@@ -66,8 +67,8 @@ export function WorkflowRunSummary({
66
67
  const status = getWorkflowStatusVisual(run.runAttempt.status);
67
68
  const action = workflowRunActionForRun(run);
68
69
  const attemptSwitcher =
69
- latestAttempt && latestAttempt > 1 && workspaceId && projectId
70
- ? {workspaceId, projectId, latestAttempt}
70
+ latestAttempt && latestAttempt > 1 && workspaceSlug && projectSlug
71
+ ? {workspaceSlug, projectSlug, latestAttempt}
71
72
  : null;
72
73
  const displayDuration = run.runAttempt.displayDuration;
73
74
  const {ref: headingTextRef, isTruncated: isHeadingTruncated} =
@@ -133,10 +134,17 @@ export function WorkflowRunSummary({
133
134
  </div>
134
135
 
135
136
  <div className="col-span-2 row-start-2 flex min-w-0 items-center gap-12 overflow-hidden text-foreground-neutral-muted">
137
+ {run.number !== null ? (
138
+ <>
139
+ <WorkflowRunNumberLabel run={run} />
140
+ {attemptSwitcher || run.triggerDisplayLabel ? <MetadataSeparator /> : null}
141
+ </>
142
+ ) : null}
143
+
136
144
  {attemptSwitcher ? (
137
145
  <WorkflowRunAttemptSwitcher
138
- workspaceId={attemptSwitcher.workspaceId}
139
- projectId={attemptSwitcher.projectId}
146
+ workspaceSlug={attemptSwitcher.workspaceSlug}
147
+ projectSlug={attemptSwitcher.projectSlug}
140
148
  run={run}
141
149
  latestAttempt={attemptSwitcher.latestAttempt}
142
150
  />
@@ -174,7 +182,9 @@ export function WorkflowRunSummary({
174
182
  </>
175
183
  ) : null}
176
184
 
177
- {attemptSwitcher || run.triggerDisplayLabel ? <MetadataSeparator /> : null}
185
+ {run.number !== null || attemptSwitcher || run.triggerDisplayLabel ? (
186
+ <MetadataSeparator />
187
+ ) : null}
178
188
  <RelativeTime
179
189
  value={run.runAttempt.createdAt}
180
190
  className="shrink-0 whitespace-nowrap text-xs leading-20 text-foreground-neutral-muted"
@@ -12,7 +12,6 @@ import {within} from 'storybook/test';
12
12
  import type {AgentStepConfig, StepError} from '#core/workflow-run.js';
13
13
  import {AgentConfigFailureCallout} from './agent-config-failure-callout.js';
14
14
 
15
- const WORKSPACE_ID = '44444444-4444-4444-8444-444444444444';
16
15
  const AGENTS_LINK_NAME = 'Configure Agents';
17
16
 
18
17
  const config: AgentStepConfig = {
@@ -42,7 +41,7 @@ const meta = {
42
41
  });
43
42
  const agentSettingsRoute = createRoute({
44
43
  getParentRoute: () => rootRoute,
45
- path: '/workspaces/$wid/settings/agents',
44
+ path: '/w/$workspaceSlug/settings/agents',
46
45
  component: () => null,
47
46
  });
48
47
  const router = createRouter({
@@ -54,7 +53,7 @@ const meta = {
54
53
  },
55
54
  ],
56
55
  args: {
57
- workspaceId: WORKSPACE_ID,
56
+ workspaceSlug: 'acme',
58
57
  config,
59
58
  error: makeError('provider_not_configured'),
60
59
  },
@@ -10,11 +10,11 @@ import {Link} from '@tanstack/react-router';
10
10
  import type {AgentStepConfig, StepError} from '#core/workflow-run.js';
11
11
 
12
12
  export function AgentConfigFailureCallout({
13
- workspaceId,
13
+ workspaceSlug,
14
14
  config,
15
15
  error,
16
16
  }: {
17
- workspaceId: string;
17
+ workspaceSlug?: string | undefined;
18
18
  config: AgentStepConfig | null;
19
19
  error: StepError | null;
20
20
  }) {
@@ -25,10 +25,10 @@ export function AgentConfigFailureCallout({
25
25
  <AlertContent>
26
26
  <AlertTitle>{copy.title}</AlertTitle>
27
27
  <AlertDescription>{copy.description}</AlertDescription>
28
- {copy.showProviderCta ? (
28
+ {copy.showProviderCta && workspaceSlug ? (
29
29
  <AlertActions>
30
30
  <Button asChild size="2xs" variant="secondary" iconRight="chevronRight">
31
- <Link to="/workspaces/$wid/settings/agents" params={{wid: workspaceId}}>
31
+ <Link to="/w/$workspaceSlug/settings/agents" params={{workspaceSlug}}>
32
32
  Configure Agents
33
33
  </Link>
34
34
  </Button>
@@ -18,7 +18,6 @@ import {
18
18
  import type {StepExpandedContext} from '../step-list/index.js';
19
19
  import {JobCard} from './job-card.js';
20
20
 
21
- const WORKSPACE_ID = '44444444-4444-4444-8444-444444444444';
22
21
  const SWITCH_EXECUTION_PATTERN = /Switch job execution/;
23
22
  const EXECUTION_FIVE_PATTERN = /#5/;
24
23
 
@@ -41,7 +40,6 @@ const meta = {
41
40
  ),
42
41
  ],
43
42
  args: {
44
- workspaceId: WORKSPACE_ID,
45
43
  job: singleExecutionJob(),
46
44
  selectedJobExecution: singleExecutionJob().jobExecutions[0],
47
45
  selectedAttemptId: null,
@@ -73,7 +71,7 @@ export const States: Story = {
73
71
  <StorySection label="single execution">
74
72
  <JobCardStory job={singleExecutionJob()} />
75
73
  </StorySection>
76
- <StorySection label="display name fallback">
74
+ <StorySection label="execution name with missing job name">
77
75
  <JobCardStory job={keyFallbackJob()} />
78
76
  </StorySection>
79
77
  <StorySection label="queueing execution">
@@ -126,13 +124,13 @@ export const ListeningProgression: Story = {
126
124
  <StorySection label="pending, no executions">
127
125
  <JobCardStory job={listeningJobPending()} />
128
126
  </StorySection>
129
- <StorySection label="running, 0 executions">
127
+ <StorySection label="listening, 0 executions">
130
128
  <JobCardStory job={listeningJobNoExecutions()} />
131
129
  </StorySection>
132
- <StorySection label="running, 1 running execution">
130
+ <StorySection label="listening, 1 running execution">
133
131
  <JobCardStory job={listeningJobOneExecution()} initialJobExecutionId="exec-listen-one-1" />
134
132
  </StorySection>
135
- <StorySection label="running, mixed executions">
133
+ <StorySection label="listening, mixed executions">
136
134
  <JobCardStory job={listeningJobMixedExecutions()} initialJobExecutionId="exec-listen-6" />
137
135
  </StorySection>
138
136
  <StorySection label="failed, resolved">
@@ -145,19 +143,19 @@ export const ListeningProgression: Story = {
145
143
  export const ManyExecutions: Story = {
146
144
  render: () => (
147
145
  <div className="flex flex-col gap-16">
148
- <StorySection label="running, 64 executions, short name">
146
+ <StorySection label="listening, 64 executions, short name">
149
147
  <JobCardStory
150
148
  job={manyExecutionsListeningJob('gate', 'job-many-short')}
151
149
  initialJobExecutionId="exec-many-job-many-short-64"
152
150
  />
153
151
  </StorySection>
154
- <StorySection label="running, 64 executions, medium name">
152
+ <StorySection label="listening, 64 executions, medium name">
155
153
  <JobCardStory
156
154
  job={manyExecutionsListeningJob('release-gates', 'job-many-medium')}
157
155
  initialJobExecutionId="exec-many-job-many-medium-64"
158
156
  />
159
157
  </StorySection>
160
- <StorySection label="running, 64 executions, long name">
158
+ <StorySection label="listening, 64 executions, long name">
161
159
  <JobCardStory
162
160
  job={manyExecutionsListeningJob(
163
161
  'release-production-multi-region-with-canary-validation',
@@ -233,7 +231,6 @@ function JobCardStory({
233
231
 
234
232
  return (
235
233
  <JobCard
236
- workspaceId={WORKSPACE_ID}
237
234
  job={job}
238
235
  selectedJobExecution={selectedJobExecution}
239
236
  selectedAttemptId={selectedAttemptId}
@@ -715,6 +712,10 @@ function makeTriggerEvent(
715
712
  event,
716
713
  delivery_id: `delivery-${sequence}`,
717
714
  received_at: listeningTimestamp(sequence, 1),
715
+ project: null,
716
+ repository: null,
717
+ ref: null,
718
+ commit: null,
718
719
  data: {sequence},
719
720
  };
720
721
  }
@@ -0,0 +1,115 @@
1
+ import {render, screen} from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import {workflowJob, workflowJobExecutionDto} from '#test/fixtures/workflow-run.js';
4
+ import {JobCard} from './job-card.js';
5
+ import {JobExecutionSwitcher} from './job-execution-switcher.js';
6
+
7
+ const FIRST_EXECUTION_ACCESSIBLE_NAME =
8
+ /Execution #1: Review PR #476, Failed, validation failed, duration 1m 55s/;
9
+ const SECOND_EXECUTION_MENU_ITEM = /Execution #2: Review PR #479/;
10
+
11
+ describe('JobCard execution names', () => {
12
+ test('shows only the resolved execution name', () => {
13
+ const job = workflowJob({
14
+ key: 'deploy',
15
+ name: 'Deploy application',
16
+ job_executions: [
17
+ workflowJobExecutionDto({
18
+ name: 'Deploy production',
19
+ sequence: 1,
20
+ }),
21
+ ],
22
+ });
23
+ const execution = job.jobExecutions[0];
24
+ if (!execution) throw new Error('Expected a job execution');
25
+
26
+ render(
27
+ <JobCard
28
+ job={job}
29
+ selectedJobExecution={execution}
30
+ selectedAttemptId={null}
31
+ onSelectedJobExecutionChange={undefined}
32
+ onSelectedAttemptChange={undefined}
33
+ />,
34
+ );
35
+
36
+ expect(screen.getByRole('heading', {name: 'Deploy production'})).toBeInTheDocument();
37
+ expect(screen.queryByText('Deploy application')).not.toBeInTheDocument();
38
+ expect(screen.queryByText('Execution #1')).not.toBeInTheDocument();
39
+ });
40
+
41
+ test('uses the static job name only when no execution exists', () => {
42
+ const job = workflowJob({
43
+ key: 'deploy',
44
+ name: 'Deploy application',
45
+ job_executions: [],
46
+ });
47
+
48
+ render(
49
+ <JobCard
50
+ job={job}
51
+ selectedJobExecution={undefined}
52
+ selectedAttemptId={null}
53
+ onSelectedJobExecutionChange={undefined}
54
+ onSelectedAttemptChange={undefined}
55
+ />,
56
+ );
57
+
58
+ expect(screen.getByRole('heading', {name: 'Deploy application'})).toBeInTheDocument();
59
+ expect(screen.getByText('Waiting for this job to start')).toBeInTheDocument();
60
+ });
61
+ });
62
+
63
+ describe('JobExecutionSwitcher execution names', () => {
64
+ test('uses each listening execution name while keeping its sequence and status', async () => {
65
+ const user = userEvent.setup();
66
+ const job = workflowJob({
67
+ key: 'reviews',
68
+ name: 'Process review',
69
+ mode: 'listening',
70
+ status: 'running',
71
+ listener_status: 'listening',
72
+ job_executions: [
73
+ workflowJobExecutionDto({
74
+ sequence: 1,
75
+ name: 'Review PR #476',
76
+ status: 'failed',
77
+ status_reason: 'validation failed',
78
+ queued_at: '2026-06-21T12:00:00.000Z',
79
+ started_at: '2026-06-21T12:00:05.000Z',
80
+ finished_at: '2026-06-21T12:02:00.000Z',
81
+ }),
82
+ workflowJobExecutionDto({
83
+ sequence: 2,
84
+ name: 'Review PR #479',
85
+ status: 'running',
86
+ }),
87
+ ],
88
+ });
89
+ const selected = job.jobExecutions[1];
90
+ if (!selected) throw new Error('Expected a selected job execution');
91
+
92
+ render(
93
+ <JobExecutionSwitcher
94
+ job={job}
95
+ selectedJobExecution={selected.id}
96
+ onSelectedJobExecutionChange={() => undefined}
97
+ variant="title"
98
+ />,
99
+ );
100
+
101
+ const trigger = screen.getByRole('button', {
102
+ name: 'Switch job execution, currently execution 2: Review PR #479',
103
+ });
104
+ expect(trigger).toBeInTheDocument();
105
+
106
+ await user.click(trigger);
107
+
108
+ expect(
109
+ await screen.findByRole('menuitem', {
110
+ name: FIRST_EXECUTION_ACCESSIBLE_NAME,
111
+ }),
112
+ ).toBeInTheDocument();
113
+ expect(screen.getByRole('menuitem', {name: SECOND_EXECUTION_MENU_ITEM})).toBeInTheDocument();
114
+ });
115
+ });
@@ -12,7 +12,11 @@ import {Fragment, useId, useRef, useState} from 'react';
12
12
  import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
13
13
  import {
14
14
  AGENT_CONFIG_ISSUES,
15
+ defaultJobExecution,
16
+ deriveJobDisplayStatus,
17
+ deriveJobExecutionDisplayStatus,
15
18
  type Job,
19
+ type JobDisplayStatus,
16
20
  type JobExecution,
17
21
  type JobExecutionTime,
18
22
  STEP_ERROR_REASONS,
@@ -30,7 +34,9 @@ import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time
30
34
  import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
31
35
 
32
36
  const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
33
- ...WORKFLOW_JOB_STATUSES.map((status) => getWorkflowStatusVisual(status).label.length),
37
+ ...[...WORKFLOW_JOB_STATUSES, 'listening' as const].map(
38
+ (status) => getWorkflowStatusVisual(status).label.length,
39
+ ),
34
40
  );
35
41
 
36
42
  interface LocalSelectedAttempt {
@@ -39,7 +45,7 @@ interface LocalSelectedAttempt {
39
45
  }
40
46
 
41
47
  export function JobCard({
42
- workspaceId,
48
+ workspaceSlug,
43
49
  job,
44
50
  selectedJobExecution,
45
51
  selectedAttemptId,
@@ -51,7 +57,7 @@ export function JobCard({
51
57
  focusedSourceStepId,
52
58
  onOpenStepSource,
53
59
  }: {
54
- workspaceId: string;
60
+ workspaceSlug?: string | undefined;
55
61
  job: Job;
56
62
  selectedJobExecution: JobExecution | undefined;
57
63
  selectedAttemptId: string | null | undefined;
@@ -71,7 +77,12 @@ export function JobCard({
71
77
  jobExecutionId: undefined,
72
78
  attemptId: null,
73
79
  });
74
- const selectedExecutionStatus = selectedJobExecution?.status ?? job.status;
80
+ const title = selectedJobExecution?.displayName ?? job.displayName;
81
+ const isNonDefaultExecutionSelected =
82
+ selectedJobExecution !== undefined && selectedJobExecution.id !== defaultJobExecution(job)?.id;
83
+ const selectedExecutionStatus = isNonDefaultExecutionSelected
84
+ ? deriveJobExecutionDisplayStatus(selectedJobExecution)
85
+ : deriveJobDisplayStatus(job);
75
86
  const effectiveSelectedAttemptId =
76
87
  selectedAttemptId === undefined &&
77
88
  localSelectedAttempt.jobExecutionId === selectedJobExecution?.id
@@ -93,7 +104,7 @@ export function JobCard({
93
104
  <CarriedOverStepPanel />
94
105
  ) : (
95
106
  <StepAttemptDetailPanel
96
- workspaceId={workspaceId}
107
+ workspaceSlug={workspaceSlug}
97
108
  step={step}
98
109
  stepId={stepId}
99
110
  attempt={attempt}
@@ -125,7 +136,7 @@ export function JobCard({
125
136
  onSelectedJobExecutionChange ? (
126
137
  <>
127
138
  <Text as="h2" id={titleId} size="sm" bold className="sr-only">
128
- {job.displayName}
139
+ {title}
129
140
  </Text>
130
141
  <div className="flex min-w-0 items-center gap-8">
131
142
  <JobStatusBadge status={selectedExecutionStatus} />
@@ -140,15 +151,17 @@ export function JobCard({
140
151
  ) : (
141
152
  <div className="flex min-w-0 items-center gap-8">
142
153
  <JobStatusBadge status={selectedExecutionStatus} />
143
- <Text
144
- as="h2"
145
- id={titleId}
146
- size="sm"
147
- bold
148
- className="min-w-0 truncate text-foreground-neutral-base"
149
- >
150
- {job.displayName}
151
- </Text>
154
+ <div className="flex min-w-0 items-center gap-8">
155
+ <Text
156
+ as="h2"
157
+ id={titleId}
158
+ size="sm"
159
+ bold
160
+ className="min-w-0 truncate text-foreground-neutral-base"
161
+ >
162
+ {title}
163
+ </Text>
164
+ </div>
152
165
  </div>
153
166
  )}
154
167
  {sourceAvailable && selectedSourceAction && sourcePanelId && onOpenStepSource ? (
@@ -196,7 +209,7 @@ export function JobCard({
196
209
  );
197
210
  }
198
211
 
199
- function JobStatusBadge({status}: {status: Job['status'] | JobExecution['status']}) {
212
+ function JobStatusBadge({status}: {status: JobDisplayStatus}) {
200
213
  const visual = getWorkflowStatusVisual(status);
201
214
 
202
215
  return (
@@ -252,7 +265,12 @@ function JobExecutionMetadata({execution}: {execution: JobExecution | undefined}
252
265
  ) : item.kind === 'trigger' ? (
253
266
  <JobExecutionTriggerMetadata execution={execution} />
254
267
  ) : (
255
- <JobExecutionMetadataItem icon={item.icon} kind={item.kind} time={item.time} />
268
+ <JobExecutionMetadataItem
269
+ icon={item.icon}
270
+ kind={item.kind}
271
+ time={item.time}
272
+ displayStatus={deriveJobExecutionDisplayStatus(execution)}
273
+ />
256
274
  )}
257
275
  </Fragment>
258
276
  ))}
@@ -309,14 +327,16 @@ function JobExecutionMetadataItem({
309
327
  icon,
310
328
  kind,
311
329
  time,
330
+ displayStatus,
312
331
  }: {
313
332
  icon: 'hourglassLine' | 'timerLine';
314
333
  kind: 'queue' | 'run';
315
334
  time: JobExecutionTime;
335
+ displayStatus: JobExecution['status'];
316
336
  }) {
317
337
  useTimeTick();
318
338
 
319
- const tooltip = `${jobExecutionTimeVerb(kind, time)} for ${formatJobExecutionTime(time)}`;
339
+ const tooltip = `${jobExecutionTimeVerb(kind, time, displayStatus)} for ${formatJobExecutionTime(time)}`;
320
340
 
321
341
  return (
322
342
  <Tooltip>
@@ -341,9 +361,15 @@ function JobExecutionMetadataItem({
341
361
  );
342
362
  }
343
363
 
344
- function jobExecutionTimeVerb(kind: 'queue' | 'run', time: JobExecutionTime): string {
345
- if (kind === 'queue') return time.state === 'live' ? 'Queuing' : 'Queued';
346
- return time.state === 'live' ? 'Running' : 'Ran';
364
+ function jobExecutionTimeVerb(
365
+ kind: 'queue' | 'run',
366
+ time: JobExecutionTime,
367
+ displayStatus: JobExecution['status'],
368
+ ): string {
369
+ if (kind === 'queue') return time.state === 'live' ? 'Queueing' : 'Queued';
370
+ if (time.state !== 'live') return 'Ran';
371
+ if (displayStatus === 'running') return 'Running';
372
+ return displayStatus === 'pending' ? 'Runner assigned' : 'Execution elapsed';
347
373
  }
348
374
 
349
375
  function MetadataSeparator() {
@@ -369,14 +395,14 @@ function selectedStepSourceAction(
369
395
  }
370
396
 
371
397
  function StepAttemptDetailPanel({
372
- workspaceId,
398
+ workspaceSlug,
373
399
  step,
374
400
  stepId,
375
401
  attempt,
376
402
  attemptError,
377
403
  attemptStatus,
378
404
  }: {
379
- workspaceId: string;
405
+ workspaceSlug?: string | undefined;
380
406
  step: Step;
381
407
  stepId: string;
382
408
  attempt: number;
@@ -389,7 +415,7 @@ function StepAttemptDetailPanel({
389
415
  <div className="flex min-w-0 flex-col gap-10">
390
416
  {isAgentConfigFailure(step, selectedAttemptError) ? (
391
417
  <AgentConfigFailureCallout
392
- workspaceId={workspaceId}
418
+ workspaceSlug={workspaceSlug}
393
419
  config={step.agentConfig}
394
420
  error={selectedAttemptError}
395
421
  />
@@ -432,7 +458,7 @@ function toSelectedAttemptError(
432
458
  signal: typeof error.signal === 'string' ? error.signal : undefined,
433
459
  reason: resolvedReason,
434
460
  agentConfigIssue,
435
- category: step.type === 'setup' ? 'setup' : 'user',
461
+ category: step.type === 'setup' || step.type === 'checkout' ? 'setup' : 'user',
436
462
  };
437
463
  }
438
464
 
@@ -449,35 +475,48 @@ function emptyStateForJob(job: Job, jobExecution: JobExecution): StepListEmptySt
449
475
  };
450
476
  }
451
477
 
452
- if (jobExecution.status === 'pending') {
478
+ const displayStatus =
479
+ job.mode === 'listening'
480
+ ? deriveJobExecutionDisplayStatus(jobExecution)
481
+ : deriveJobDisplayStatus(job);
482
+
483
+ if (jobExecution.status === 'running' && displayStatus === 'pending') {
484
+ return {
485
+ title: 'Runner preparing job',
486
+ description: 'A runner is assigned. Steps will appear here when work begins.',
487
+ status: displayStatus,
488
+ };
489
+ }
490
+
491
+ if (displayStatus === 'pending') {
453
492
  return {
454
493
  title: 'Waiting for this job to start',
455
494
  description: 'Steps will appear here once the job starts.',
456
- status: jobExecution.status,
495
+ status: displayStatus,
457
496
  };
458
497
  }
459
498
 
460
- if (jobExecution.status === 'running') {
499
+ if (displayStatus === 'running') {
461
500
  return {
462
501
  title: 'Waiting for the first step',
463
- description: 'This job is running, but no steps have started yet.',
464
- status: jobExecution.status,
502
+ description: 'The first running step will appear here shortly.',
503
+ status: displayStatus,
465
504
  };
466
505
  }
467
506
 
468
- if (jobExecution.status === 'cancelled') {
507
+ if (displayStatus === 'cancelled') {
469
508
  return {
470
509
  title: 'Cancelled before start',
471
510
  description: 'This job was cancelled before any step started.',
472
- status: jobExecution.status,
511
+ status: displayStatus,
473
512
  };
474
513
  }
475
514
 
476
- if (jobExecution.status === 'succeeded' || jobExecution.status === 'failed') {
515
+ if (displayStatus === 'succeeded' || displayStatus === 'failed') {
477
516
  return {
478
517
  title: 'No steps recorded',
479
518
  description: `Execution #${jobExecution.sequence} finished without recorded steps.`,
480
- status: jobExecution.status,
519
+ status: displayStatus,
481
520
  };
482
521
  }
483
522
 
@@ -497,7 +536,7 @@ function emptyStateForMissingExecution(job: Job): StepListEmptyState {
497
536
  return {
498
537
  title: 'Waiting for trigger events',
499
538
  description: 'Matching trigger events will create job executions here.',
500
- status: 'running',
539
+ status: deriveJobDisplayStatus(job),
501
540
  };
502
541
  }
503
542
 
@@ -9,9 +9,10 @@ import {Icon} from '@shipfox/react-ui/icon';
9
9
  import {Text} from '@shipfox/react-ui/typography';
10
10
  import {cn} from '@shipfox/react-ui/utils';
11
11
  import {useMemo} from 'react';
12
+ import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
12
13
  import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
13
- import type {Job, JobExecution} from '#core/workflow-run.js';
14
- import {JobExecutionTimeText} from './job-execution-time-text.js';
14
+ import {deriveJobExecutionDisplayStatus, type Job, type JobExecution} from '#core/workflow-run.js';
15
+ import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time-text.js';
15
16
 
16
17
  export interface JobExecutionSwitcherProps {
17
18
  job: Job;
@@ -62,10 +63,10 @@ export function JobExecutionSwitcher({
62
63
  : 'min-h-28 gap-6 px-8 py-4 text-sm leading-20 text-foreground-neutral-subtle hover:bg-background-components-hover',
63
64
  className,
64
65
  )}
65
- aria-label={`Switch job execution, currently execution ${selected.sequence}`}
66
+ aria-label={`Switch job execution, currently execution ${selected.sequence}: ${selected.displayName}`}
66
67
  >
67
68
  {variant === 'title' ? (
68
- <TitleExecutionSummary job={job} execution={selected} />
69
+ <TitleExecutionSummary execution={selected} />
69
70
  ) : (
70
71
  <ExecutionSummary execution={selected} />
71
72
  )}
@@ -87,14 +88,19 @@ export function JobExecutionSwitcher({
87
88
  <button
88
89
  type="button"
89
90
  aria-current={isSelected ? 'true' : undefined}
91
+ aria-label={executionAccessibleLabel(jobExecution)}
90
92
  className="w-full text-left"
91
93
  >
92
- <WorkflowStatusIcon status={jobExecution.status} size={14} tooltip={false} />
94
+ <WorkflowStatusIcon
95
+ status={deriveJobExecutionDisplayStatus(jobExecution)}
96
+ size={14}
97
+ tooltip={false}
98
+ />
93
99
  <span className="font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
94
100
  #{jobExecution.sequence}
95
101
  </span>
96
102
  <span className="min-w-0 truncate text-xs leading-20 text-foreground-neutral-base">
97
- {job.displayName}
103
+ {jobExecution.displayName}
98
104
  </span>
99
105
  {!isSelected && jobExecution.statusReason ? (
100
106
  <span className="min-w-0 flex-1 truncate text-xs leading-20 text-foreground-neutral-muted">
@@ -119,23 +125,39 @@ export function JobExecutionSwitcher({
119
125
  );
120
126
  }
121
127
 
122
- function TitleExecutionSummary({job, execution}: {job: Job; execution: JobExecution}) {
128
+ function executionAccessibleLabel(execution: JobExecution): string {
129
+ const status = getWorkflowStatusVisual(deriveJobExecutionDisplayStatus(execution));
130
+ return [
131
+ `Execution #${execution.sequence}: ${execution.displayName}`,
132
+ status.label,
133
+ execution.statusReason ?? undefined,
134
+ execution.displayDuration
135
+ ? `duration ${formatJobExecutionTime(execution.displayDuration)}`
136
+ : undefined,
137
+ ]
138
+ .filter((part): part is string => Boolean(part))
139
+ .join(', ');
140
+ }
141
+
142
+ function TitleExecutionSummary({execution}: {execution: JobExecution}) {
123
143
  return (
124
144
  <span className="flex min-w-0 items-center gap-8">
125
145
  <Text as="span" size="sm" bold className="shrink-0 text-foreground-neutral-base">
126
146
  #{execution.sequence}
127
147
  </Text>
128
148
  <Text as="span" size="sm" bold className="min-w-0 truncate text-foreground-neutral-base">
129
- {job.displayName}
149
+ {execution.displayName}
130
150
  </Text>
131
151
  </span>
132
152
  );
133
153
  }
134
154
 
135
155
  function ExecutionSummary({execution}: {execution: JobExecution}) {
156
+ const displayStatus = deriveJobExecutionDisplayStatus(execution);
157
+
136
158
  return (
137
159
  <span className="flex min-w-0 items-center gap-6">
138
- <WorkflowStatusIcon status={execution.status} size={14} tooltip={false} />
160
+ <WorkflowStatusIcon status={displayStatus} size={14} tooltip={false} />
139
161
  <span className="shrink-0 font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
140
162
  Execution #{execution.sequence}
141
163
  </span>