@shipfox/client-workflows 14.0.1 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.storybook/preview.tsx +7 -2
  3. package/.turbo/turbo-build.log +1 -1
  4. package/CHANGELOG.md +42 -0
  5. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
  6. package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
  7. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
  8. package/dist/components/job-detail/job-detail-header.d.ts +15 -0
  9. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
  10. package/dist/components/job-detail/job-detail-header.js +124 -0
  11. package/dist/components/job-detail/job-detail-header.js.map +1 -0
  12. package/dist/components/job-detail/job-detail-view.d.ts +16 -0
  13. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
  14. package/dist/components/job-detail/job-detail-view.js +420 -0
  15. package/dist/components/job-detail/job-detail-view.js.map +1 -0
  16. package/dist/components/job-detail/job-empty-states.d.ts +10 -0
  17. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
  18. package/dist/components/job-detail/job-empty-states.js +195 -0
  19. package/dist/components/job-detail/job-empty-states.js.map +1 -0
  20. package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
  21. package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
  22. package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
  23. package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
  24. package/dist/components/job-detail/job-selection.d.ts +24 -0
  25. package/dist/components/job-detail/job-selection.d.ts.map +1 -0
  26. package/dist/components/job-detail/job-selection.js +66 -0
  27. package/dist/components/job-detail/job-selection.js.map +1 -0
  28. package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
  29. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
  30. package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
  31. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
  32. package/dist/components/job-graph/job-graph-content.js +1 -1
  33. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  34. package/dist/components/step-list/step-list-model.js +2 -2
  35. package/dist/components/step-list/step-list-model.js.map +1 -1
  36. package/dist/components/step-list/step-list.d.ts +1 -0
  37. package/dist/components/step-list/step-list.d.ts.map +1 -1
  38. package/dist/components/step-list/step-list.js +29 -4
  39. package/dist/components/step-list/step-list.js.map +1 -1
  40. package/dist/components/workflow-run-duration-label.d.ts +9 -4
  41. package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
  42. package/dist/components/workflow-run-duration-label.js +27 -7
  43. package/dist/components/workflow-run-duration-label.js.map +1 -1
  44. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  45. package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
  46. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  47. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  48. package/dist/components/workflow-run-summary/workflow-run-summary.js +69 -64
  49. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  50. package/dist/components/workflow-run-tabs/index.d.ts +3 -3
  51. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  52. package/dist/components/workflow-run-tabs/index.js +3 -3
  53. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  54. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  55. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  56. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  57. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  58. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  59. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  60. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  61. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  62. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
  63. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  64. package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
  65. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  66. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  67. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  68. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
  69. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  70. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
  71. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
  72. package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
  73. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
  74. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  75. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  76. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  77. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  78. package/dist/components/workflow-run-view/index.d.ts +0 -1
  79. package/dist/components/workflow-run-view/index.d.ts.map +1 -1
  80. package/dist/components/workflow-run-view/index.js +0 -1
  81. package/dist/components/workflow-run-view/index.js.map +1 -1
  82. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
  83. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
  84. package/dist/components/workflow-run-view/run-workspace-nav.js +277 -0
  85. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -0
  86. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  87. package/dist/components/workflow-run-view/workflow-run-selection.js +1 -14
  88. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -10
  90. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  91. package/dist/components/workflow-run-view/workflow-run-view.js +351 -239
  92. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  93. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  94. package/dist/components/workflow-status/status-visuals.js +10 -2
  95. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  96. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
  97. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  98. package/dist/components/workflow-status/workflow-status-icon.js +13 -2
  99. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  100. package/dist/core/entities/job-execution.d.ts +9 -0
  101. package/dist/core/entities/job-execution.d.ts.map +1 -1
  102. package/dist/core/entities/job-execution.js +13 -3
  103. package/dist/core/entities/job-execution.js.map +1 -1
  104. package/dist/core/entities/job.d.ts.map +1 -1
  105. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  106. package/dist/core/entities/step.d.ts +2 -0
  107. package/dist/core/entities/step.d.ts.map +1 -1
  108. package/dist/core/entities/step.js +13 -0
  109. package/dist/core/entities/step.js.map +1 -1
  110. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  111. package/dist/core/entities/workflow-run.d.ts +56 -1
  112. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  113. package/dist/core/entities/workflow-run.js +98 -0
  114. package/dist/core/entities/workflow-run.js.map +1 -1
  115. package/dist/core/run-annotation.d.ts +73 -0
  116. package/dist/core/run-annotation.d.ts.map +1 -0
  117. package/dist/core/run-annotation.js +125 -0
  118. package/dist/core/run-annotation.js.map +1 -0
  119. package/dist/core/workflow-run.d.ts +4 -4
  120. package/dist/core/workflow-run.d.ts.map +1 -1
  121. package/dist/core/workflow-run.js +3 -3
  122. package/dist/core/workflow-run.js.map +1 -1
  123. package/dist/feature.d.ts +20 -16
  124. package/dist/feature.d.ts.map +1 -1
  125. package/dist/feature.js +5 -0
  126. package/dist/feature.js.map +1 -1
  127. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  128. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  129. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  130. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  131. package/dist/hooks/api/run-annotations.d.ts +25 -0
  132. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  133. package/dist/hooks/api/run-annotations.js +78 -0
  134. package/dist/hooks/api/run-annotations.js.map +1 -0
  135. package/dist/hooks/api/workflow-runs.d.ts +5 -3
  136. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  137. package/dist/hooks/api/workflow-runs.js +5 -4
  138. package/dist/hooks/api/workflow-runs.js.map +1 -1
  139. package/dist/index.d.ts +1 -0
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +1 -0
  142. package/dist/index.js.map +1 -1
  143. package/dist/pages/workflow-job-detail-page.d.ts +11 -0
  144. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
  145. package/dist/pages/workflow-job-detail-page.js +66 -0
  146. package/dist/pages/workflow-job-detail-page.js.map +1 -0
  147. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
  148. package/dist/pages/workflow-run-detail-page.js +4 -37
  149. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +52 -34
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +74 -43
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/routes/job-detail.d.ts +10 -0
  155. package/dist/routes/job-detail.d.ts.map +1 -0
  156. package/dist/routes/job-detail.js +27 -0
  157. package/dist/routes/job-detail.js.map +1 -0
  158. package/dist/routes/project-route.d.ts +1 -1
  159. package/dist/routes/run-detail.d.ts.map +1 -1
  160. package/dist/routes/runs.d.ts.map +1 -1
  161. package/dist/routes/workflows.d.ts.map +1 -1
  162. package/dist/tsconfig.test.tsbuildinfo +1 -1
  163. package/package.json +10 -9
  164. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
  165. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
  166. package/src/components/job-detail/job-detail-header.test.tsx +34 -0
  167. package/src/components/job-detail/job-detail-header.tsx +136 -0
  168. package/src/components/job-detail/job-detail-view.stories.tsx +531 -0
  169. package/src/components/job-detail/job-detail-view.tsx +467 -0
  170. package/src/components/job-detail/job-empty-states.test.ts +27 -0
  171. package/src/components/job-detail/job-empty-states.tsx +248 -0
  172. package/src/components/job-detail/job-selection.test.ts +188 -0
  173. package/src/components/job-detail/job-selection.ts +96 -0
  174. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
  175. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
  176. package/src/components/job-graph/job-graph-content.tsx +1 -1
  177. package/src/components/step-list/step-list-model.test.ts +1 -1
  178. package/src/components/step-list/step-list-model.ts +2 -2
  179. package/src/components/step-list/step-list.stories.tsx +1 -1
  180. package/src/components/step-list/step-list.test.tsx +33 -0
  181. package/src/components/step-list/step-list.tsx +40 -9
  182. package/src/components/workflow-run-duration-label.tsx +50 -12
  183. package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
  184. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
  185. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
  186. package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
  187. package/src/components/workflow-run-tabs/index.ts +3 -3
  188. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  189. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  190. package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
  191. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
  193. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
  194. package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
  195. package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
  196. package/src/components/workflow-run-view/index.ts +0 -1
  197. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +286 -0
  198. package/src/components/workflow-run-view/run-workspace-nav.tsx +314 -0
  199. package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
  200. package/src/components/workflow-run-view/workflow-run-view.test.tsx +237 -1085
  201. package/src/components/workflow-run-view/workflow-run-view.tsx +450 -323
  202. package/src/components/workflow-status/status-visuals.ts +5 -2
  203. package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
  204. package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
  205. package/src/core/entities/job-execution.ts +17 -2
  206. package/src/core/entities/step.ts +26 -0
  207. package/src/core/entities/workflow-run-display.test.ts +159 -0
  208. package/src/core/entities/workflow-run.ts +139 -1
  209. package/src/core/run-annotation.test.ts +315 -0
  210. package/src/core/run-annotation.ts +238 -0
  211. package/src/core/workflow-run.ts +16 -1
  212. package/src/feature.ts +5 -0
  213. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  214. package/src/hooks/api/run-annotations.ts +137 -0
  215. package/src/hooks/api/workflow-runs.ts +6 -2
  216. package/src/index.ts +1 -0
  217. package/src/page-harness-budget.test.ts +1 -0
  218. package/src/pages/workflow-job-detail-page.test.tsx +605 -0
  219. package/src/pages/workflow-job-detail-page.tsx +84 -0
  220. package/src/pages/workflow-run-detail-page.tsx +3 -43
  221. package/src/pages/workflow-run-pages.test.tsx +77 -332
  222. package/src/routes/inputs.test.ts +56 -6
  223. package/src/routes/inputs.ts +71 -19
  224. package/src/routes/job-detail.tsx +29 -0
  225. package/test/pages.tsx +25 -3
  226. package/test/render.tsx +6 -1
  227. package/tsconfig.build.tsbuildinfo +1 -1
  228. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
  229. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
  230. package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
  231. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
  232. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
  233. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
  234. package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
  235. package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
  236. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
  237. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
  238. package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
  239. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
  240. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
  241. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
  242. package/dist/components/workflow-run-view/job-card.d.ts +0 -17
  243. package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
  244. package/dist/components/workflow-run-view/job-card.js +0 -496
  245. package/dist/components/workflow-run-view/job-card.js.map +0 -1
  246. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
  247. package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
  248. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
  249. package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
  250. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
  251. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
  252. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
  253. package/dist/core/workflow-run-tabs.d.ts +0 -9
  254. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  255. package/dist/core/workflow-run-tabs.js +0 -3
  256. package/dist/core/workflow-run-tabs.js.map +0 -1
  257. package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
  258. package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
  259. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
  260. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
  261. package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
  262. package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
  263. package/src/components/workflow-run-view/job-card.test.tsx +0 -115
  264. package/src/components/workflow-run-view/job-card.tsx +0 -614
  265. package/src/core/workflow-run-tabs.ts +0 -8
  266. /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
  267. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
  268. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
  269. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
  270. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
  271. /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
  272. /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
@@ -2,37 +2,39 @@ import {ApiError} from '@shipfox/client-api';
2
2
  import {QueryLoadError} from '@shipfox/client-ui';
3
3
  import {EmptyState} from '@shipfox/react-ui/empty-state';
4
4
  import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
5
- import {Tabs, TabsContent, TabsContents} from '@shipfox/react-ui/tabs';
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '@shipfox/react-ui/select';
6
12
  import {toast} from '@shipfox/react-ui/toast';
13
+ import {Text} from '@shipfox/react-ui/typography';
7
14
  import {useNavigate} from '@tanstack/react-router';
8
- import {useEffect, useId, useRef, useState} from 'react';
9
- import {
10
- type JobExecution,
11
- resolveJobExecution,
12
- type StepSourceLocation,
13
- type WorkflowRunRerunMode,
14
- } from '#core/workflow-run.js';
15
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
16
- import {
17
- type WorkflowRunSelectionInput,
18
- withoutWorkflowRunSelectionSearch,
19
- } from '#core/workflow-run-url-state.js';
15
+ import {type ReactNode, useEffect, useMemo} from 'react';
16
+ import {buildRunAnnotationList, type RunAnnotationSummary} from '#core/run-annotation.js';
17
+ import {isWorkflowRunTerminal, type Job, type WorkflowRunRerunMode} from '#core/workflow-run.js';
18
+ import {withoutWorkflowRunSelectionSearch} from '#core/workflow-run-url-state.js';
19
+ import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
20
20
  import {
21
21
  useCancelWorkflowRunMutation,
22
22
  useRerunWorkflowRunMutation,
23
23
  useWorkflowRunAttemptQuery,
24
24
  } from '#hooks/api/workflow-runs.js';
25
- import {WORKFLOW_RUN_TABS, type WorkflowRunTab} from '#routes/inputs.js';
25
+ import {
26
+ type WorkflowJobSearch,
27
+ type WorkflowRunsSearch,
28
+ type WorkflowRunTab,
29
+ workflowJobSearchParams,
30
+ workflowRunSearchParams,
31
+ } from '#routes/inputs.js';
26
32
  import {JobGraph} from '../job-graph/index.js';
27
33
  import type {JobGraphSelectionSource} from '../job-graph/types.js';
28
34
  import {WorkflowRunSummary} from '../workflow-run-summary/index.js';
29
- import {
30
- RunAnnotationSummaryLine,
31
- RunAnnotationsEmpty,
32
- RunJobsList,
33
- RunTabStrip,
34
- } from '../workflow-run-tabs/index.js';
35
- import {WorkflowSourceContent, WorkflowSourcePanel} from '../workflow-source-panel/index.js';
35
+ import {RunAnnotationList, RunAnnotationSummaryLine} from '../workflow-run-tabs/index.js';
36
+ import {WorkflowSourceContent} from '../workflow-source-panel/index.js';
37
+ import {RunWorkspaceNav} from './run-workspace-nav.js';
36
38
  import {resolveWorkflowRunSelection} from './workflow-run-selection.js';
37
39
  import {
38
40
  WorkflowRunContentSkeleton,
@@ -41,40 +43,58 @@ import {
41
43
  WorkflowRunStaleError,
42
44
  } from './workflow-run-states.js';
43
45
 
44
- interface WorkflowSourceFocus {
45
- stepId: string;
46
- location: StepSourceLocation;
47
- }
46
+ type RunWorkspaceSection = Exclude<WorkflowRunTab, 'jobs'>;
48
47
 
49
48
  export interface WorkflowRunViewProps {
50
49
  projectId: string;
51
50
  workspaceSlug?: string | undefined;
52
51
  projectSlug?: string | undefined;
53
52
  workflowRunId?: string | undefined;
54
- selection?: WorkflowRunSelectionInput | undefined;
55
- onSelectionChange?: ((selection: WorkflowRunSelectionInput) => void) | undefined;
53
+ runAttempt?: number | undefined;
54
+ selection?: WorkflowRunsSearch | undefined;
56
55
  tab?: WorkflowRunTab | undefined;
57
- onTabChange?: ((tab: WorkflowRunTab) => void) | undefined;
58
- annotationSummary?: RunAnnotationSummary | undefined;
56
+ activeJobId?: string | undefined;
57
+ jobSearch?: WorkflowJobSearch | undefined;
58
+ jobContent?: ReactNode | undefined;
59
59
  }
60
60
 
61
61
  /**
62
- * Renders the run for `workflowRunId`, keeping the local tab strip mounted while the run
63
- * request is pending so polling never inserts navigation under the user's cursor.
62
+ * The persistent run workspace. Run-level sections and dedicated job routes share the same
63
+ * header and job index, while the all-jobs Summary always opens on the dependency graph.
64
64
  */
65
65
  export function WorkflowRunView({
66
66
  projectId,
67
67
  workspaceSlug,
68
68
  projectSlug,
69
69
  workflowRunId,
70
+ runAttempt,
70
71
  selection,
71
- onSelectionChange,
72
72
  tab,
73
- onTabChange,
74
- annotationSummary,
73
+ activeJobId,
74
+ jobSearch,
75
+ jobContent,
75
76
  }: WorkflowRunViewProps) {
76
- const runQuery = useWorkflowRunAttemptQuery({workflowRunId, runAttempt: selection?.runAttempt});
77
+ const runQuery = useWorkflowRunAttemptQuery({
78
+ workflowRunId,
79
+ runAttempt: selection?.runAttempt ?? runAttempt,
80
+ });
77
81
  const rerunMutation = useRerunWorkflowRunMutation(projectId);
82
+ // Annotations are read on their own cadence rather than hydrated into the run, so the two
83
+ // modules stay decoupled. The key is shared, so the job page's count chip costs no extra fetch.
84
+ //
85
+ // Polling is scoped to the surface that renders bodies. The read has no counts-only mode, so
86
+ // every poll transfers whole Markdown bodies; doing that every four seconds behind Summary,
87
+ // Source, and the job log spends megabytes to keep one rail number warm. Off that surface the
88
+ // counts still refresh on navigation and on window focus.
89
+ const annotationsQuery = useRunAnnotationsQuery({
90
+ workflowRunId,
91
+ runAttempt: runQuery.data?.runAttempt.attempt,
92
+ live:
93
+ runWorkspaceSection(tab) === 'annotations' &&
94
+ !activeJobId &&
95
+ Boolean(runQuery.data) &&
96
+ !isWorkflowRunTerminal(runQuery.data?.runAttempt.status ?? 'pending'),
97
+ });
78
98
 
79
99
  return (
80
100
  <RelativeTimeProvider>
@@ -83,12 +103,14 @@ export function WorkflowRunView({
83
103
  workspaceSlug={workspaceSlug}
84
104
  projectSlug={projectSlug}
85
105
  query={runQuery}
106
+ annotations={annotationsQuery}
86
107
  rerunMutation={rerunMutation}
108
+ runAttempt={runAttempt}
87
109
  selection={selection}
88
- onSelectionChange={onSelectionChange}
89
110
  tab={tab}
90
- onTabChange={onTabChange}
91
- annotationSummary={annotationSummary}
111
+ activeJobId={activeJobId}
112
+ jobSearch={jobSearch}
113
+ jobContent={jobContent}
92
114
  />
93
115
  </div>
94
116
  </RelativeTimeProvider>
@@ -99,108 +121,82 @@ function RunViewContent({
99
121
  workspaceSlug,
100
122
  projectSlug,
101
123
  query,
124
+ annotations,
102
125
  rerunMutation,
126
+ runAttempt,
103
127
  selection,
104
- onSelectionChange,
105
128
  tab,
106
- onTabChange,
107
- annotationSummary,
129
+ activeJobId,
130
+ jobSearch,
131
+ jobContent,
108
132
  }: {
109
133
  workspaceSlug: string | undefined;
110
134
  projectSlug: string | undefined;
111
135
  query: ReturnType<typeof useWorkflowRunAttemptQuery>;
136
+ annotations: ReturnType<typeof useRunAnnotationsQuery>;
112
137
  rerunMutation: ReturnType<typeof useRerunWorkflowRunMutation>;
113
- selection: WorkflowRunSelectionInput | undefined;
114
- onSelectionChange: ((selection: WorkflowRunSelectionInput) => void) | undefined;
138
+ runAttempt: number | undefined;
139
+ selection: WorkflowRunsSearch | undefined;
115
140
  tab: WorkflowRunTab | undefined;
116
- onTabChange: ((tab: WorkflowRunTab) => void) | undefined;
117
- annotationSummary: RunAnnotationSummary | undefined;
141
+ activeJobId: string | undefined;
142
+ jobSearch: WorkflowJobSearch | undefined;
143
+ jobContent: ReactNode | undefined;
118
144
  }) {
119
145
  const navigate = useNavigate();
120
- const [selectedJobId, setSelectedJobId] = useState<string | undefined>();
121
- const [selectedJobExecutionId, setSelectedJobExecutionId] = useState<string | undefined>();
122
- const [localTab, setLocalTab] = useState<WorkflowRunTab>(tab ?? 'summary');
123
- const [sourcePanelOpen, setSourcePanelOpen] = useState(false);
124
- const [sourceFocus, setSourceFocus] = useState<WorkflowSourceFocus | null>(null);
125
- const sourcePanelId = useId();
126
- // The step Source button that last opened the panel, so Escape / Close returns focus to it.
127
- const lastSourceTriggerRef = useRef<HTMLButtonElement | null>(null);
128
- const selectionControlled = selection !== undefined;
129
- const tabControlled = onTabChange !== undefined;
130
- const activeTab = tabControlled ? (tab ?? 'summary') : localTab;
131
- const sourceAvailable =
132
- query.data?.sourceSnapshot !== null && query.data?.sourceSnapshot !== undefined;
133
- const cancelMutation = useCancelWorkflowRunMutation(query.data);
134
-
135
- useEffect(() => {
136
- if (!sourceAvailable) {
137
- setSourcePanelOpen(false);
138
- setSourceFocus(null);
139
- }
140
- }, [sourceAvailable]);
141
-
142
- // If a refetch drops the focused step or its location, close the panel so it never points at
143
- // an unmounted Source button.
144
- useEffect(() => {
145
- if (!sourceFocus) return;
146
- const stillLocated = query.data?.jobs.some((job) =>
147
- job.jobExecutions.some((execution) =>
148
- execution.steps.some((step) => step.id === sourceFocus.stepId && step.sourceLocation),
149
- ),
150
- );
151
- if (!stillLocated) {
152
- setSourceFocus(null);
153
- setSourcePanelOpen(false);
154
- }
155
- }, [sourceFocus, query.data]);
156
-
157
146
  const runData = query.data;
158
- const resolvedSelection =
159
- selectionControlled && runData
160
- ? resolveWorkflowRunSelection({
161
- run: runData,
162
- selection: selection as WorkflowRunSelectionInput,
163
- })
164
- : undefined;
165
- const hasExplicitJobSelection = Boolean(
166
- selection?.jobId || selection?.jobExecutionId || selection?.stepId || selection?.stepAttemptId,
167
- );
168
- const selectedJob = selectionControlled
169
- ? hasExplicitJobSelection
170
- ? resolvedSelection?.job
171
- : undefined
172
- : (runData?.jobs.find((job) => job.id === selectedJobId) ?? runData?.jobs.at(0));
173
- const selectedJobExecution = selectionControlled
174
- ? resolvedSelection?.jobExecution
175
- : selectedJob
176
- ? resolveJobExecution(selectedJob, selectedJobExecutionId)
177
- : undefined;
178
- const selectedAttemptId = selectionControlled
179
- ? (resolvedSelection?.selectedAttemptId ?? null)
180
- : undefined;
181
- // Explicit per-step focus wins; fall back to the URL-selected step so deep links still
182
- // pre-highlight source in the Source tab.
183
- const highlightedLineRange =
184
- sourceFocus?.location ?? resolvedSelection?.step?.sourceLocation ?? null;
147
+ const annotationSummary = annotations.summary;
148
+ const activeSection = runWorkspaceSection(tab);
149
+ const cancelMutation = useCancelWorkflowRunMutation(runData);
185
150
  const sourceSnapshot = runData?.sourceSnapshot ?? null;
151
+ const resolvedSelection =
152
+ runData && selection ? resolveWorkflowRunSelection({run: runData, selection}) : undefined;
153
+ const highlightedLineRange = resolvedSelection?.step?.sourceLocation ?? null;
186
154
 
187
- function changeTab(nextTab: WorkflowRunTab) {
188
- if (!tabControlled) {
189
- setLocalTab(nextTab);
190
- } else if (activeTab !== nextTab) {
191
- onTabChange?.(nextTab);
192
- }
193
-
194
- if (nextTab !== 'jobs') {
195
- setSourcePanelOpen(false);
196
- setSourceFocus(null);
155
+ useEffect(() => {
156
+ const hasLegacyJobSelection = Boolean(
157
+ selection?.jobId ||
158
+ selection?.jobExecutionId ||
159
+ selection?.stepId ||
160
+ selection?.stepAttemptId,
161
+ );
162
+ if (
163
+ activeJobId ||
164
+ activeSection !== 'summary' ||
165
+ !hasLegacyJobSelection ||
166
+ !resolvedSelection?.job ||
167
+ !runData ||
168
+ !workspaceSlug ||
169
+ !projectSlug
170
+ ) {
171
+ return;
197
172
  }
198
- }
199
173
 
200
- function handleTabChange(nextTab: string) {
201
- if (!WORKFLOW_RUN_TABS.some((value) => value === nextTab)) return;
202
- changeTab(nextTab as WorkflowRunTab);
203
- }
174
+ void navigate({
175
+ to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId',
176
+ params: {
177
+ workspaceSlug,
178
+ projectSlug,
179
+ workflowRunId: runData.id,
180
+ jobId: resolvedSelection.job.id,
181
+ },
182
+ search: workflowJobSearchParams({
183
+ jobExecutionId: selection?.jobExecutionId,
184
+ stepId: selection?.stepId,
185
+ stepAttemptId: selection?.stepAttemptId,
186
+ runAttempt: selection?.runAttempt,
187
+ }) as never,
188
+ replace: true,
189
+ });
190
+ }, [
191
+ activeJobId,
192
+ activeSection,
193
+ navigate,
194
+ projectSlug,
195
+ resolvedSelection?.job,
196
+ runData,
197
+ selection,
198
+ workspaceSlug,
199
+ ]);
204
200
 
205
201
  async function rerun(mode: WorkflowRunRerunMode) {
206
202
  if (!runData || !workspaceSlug || !projectSlug) {
@@ -212,11 +208,7 @@ function RunViewContent({
212
208
  toast.success('Re-run started');
213
209
  await navigate({
214
210
  to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
215
- params: {
216
- workspaceSlug,
217
- projectSlug,
218
- workflowRunId: run.id,
219
- },
211
+ params: {workspaceSlug, projectSlug, workflowRunId: run.id},
220
212
  search: ((previous: Record<string, unknown>) =>
221
213
  withoutWorkflowRunSelectionSearch(previous)) as never,
222
214
  });
@@ -225,91 +217,60 @@ function RunViewContent({
225
217
  }
226
218
  }
227
219
 
228
- function selectJob(jobId: string | undefined, source: JobGraphSelectionSource = 'pointer') {
229
- if (source === 'pointer') changeTab('jobs');
230
- if (!selectionControlled) {
231
- setSelectedJobId(jobId);
232
- setSelectedJobExecutionId(undefined);
233
- return;
234
- }
235
- // Keyboard roving focus is an interaction within the Summary graph. Keep that panel mounted
236
- // and let JobGraphView own the visual selection so focus is not moved to a different tab.
237
- if (source === 'keyboard') return;
238
-
239
- onSelectionChange?.(
240
- jobId ? {jobId, runAttempt: selection?.runAttempt} : {runAttempt: selection?.runAttempt},
241
- );
242
- }
243
-
244
- function selectJobExecution(jobExecutionId: string | undefined) {
245
- if (!selectionControlled) {
246
- setSelectedJobExecutionId(jobExecutionId);
247
- return;
248
- }
249
- if (!selectedJob) return;
220
+ function selectGraphJob(jobId: string | undefined, source: JobGraphSelectionSource = 'pointer') {
221
+ if (source !== 'pointer' || !jobId || !runData || !workspaceSlug || !projectSlug) return;
250
222
 
251
- onSelectionChange?.({
252
- jobId: selectedJob.id,
253
- jobExecutionId,
254
- runAttempt: selection?.runAttempt,
223
+ void navigate({
224
+ to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId',
225
+ params: {workspaceSlug, projectSlug, workflowRunId: runData.id, jobId},
226
+ search: workflowJobSearchParams({
227
+ runAttempt: selection?.runAttempt ?? runAttempt ?? runData.runAttempt.attempt,
228
+ }) as never,
255
229
  });
256
230
  }
257
231
 
258
- function selectAttempt(attemptId: string | undefined) {
259
- if (!selectionControlled || !selectedJob || !selectedJobExecution) return;
260
-
261
- if (!attemptId) {
262
- onSelectionChange?.({
263
- jobId: selectedJob.id,
264
- jobExecutionId: selectedJobExecution.id,
265
- runAttempt: selection?.runAttempt,
266
- });
267
- return;
268
- }
232
+ function selectAnnotationJob(jobId: string | undefined) {
233
+ if (!runData || !workspaceSlug || !projectSlug) return;
234
+ const nextSearch: WorkflowRunsSearch = {...selection, tab: 'annotations'};
235
+ if (jobId) nextSearch.jobId = jobId;
236
+ else delete nextSearch.jobId;
237
+ delete nextSearch.jobExecutionId;
238
+ delete nextSearch.stepId;
239
+ delete nextSearch.stepAttemptId;
269
240
 
270
- const match = findAttemptSelection(selectedJobExecution, attemptId);
271
- if (!match) return;
272
-
273
- onSelectionChange?.({
274
- jobId: selectedJob.id,
275
- jobExecutionId: selectedJobExecution.id,
276
- stepId: match.stepId,
277
- stepAttemptId: match.attemptId,
278
- runAttempt: selection?.runAttempt,
241
+ void navigate({
242
+ to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
243
+ params: {workspaceSlug, projectSlug, workflowRunId: runData.id},
244
+ search: workflowRunSearchParams(nextSearch, nextSearch) as never,
279
245
  });
280
246
  }
281
247
 
282
- function openStepSource(
283
- stepId: string,
284
- location: StepSourceLocation,
285
- trigger: HTMLButtonElement | null,
286
- ) {
287
- setSourceFocus({stepId, location});
288
- lastSourceTriggerRef.current = trigger;
289
- setSourcePanelOpen(true);
290
- }
248
+ function clearAnnotationFilters() {
249
+ if (!runData || !workspaceSlug || !projectSlug) return;
250
+ const nextSearch: WorkflowRunsSearch = {...selection, tab: 'annotations'};
251
+ delete nextSearch.jobId;
252
+ delete nextSearch.jobExecutionId;
253
+ delete nextSearch.stepId;
254
+ delete nextSearch.stepAttemptId;
255
+ delete nextSearch.severity;
256
+ delete nextSearch.annotation;
291
257
 
292
- function closeSourcePanel() {
293
- const trigger = lastSourceTriggerRef.current;
294
- setSourcePanelOpen(false);
295
- // Defer so focus lands after the panel unmounts; clear the focus only after focusing so the
296
- // opener button is still expanded (force-visible) on return.
297
- window.setTimeout(() => {
298
- if (trigger?.isConnected) trigger.focus();
299
- setSourceFocus(null);
300
- }, 0);
258
+ void navigate({
259
+ to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
260
+ params: {workspaceSlug, projectSlug, workflowRunId: runData.id},
261
+ search: workflowRunSearchParams(nextSearch, nextSearch) as never,
262
+ replace: true,
263
+ });
301
264
  }
302
265
 
303
266
  function cancelRun() {
304
267
  cancelMutation.mutate(undefined, {
305
- onError: (error) => {
306
- toast.error(cancelErrorMessage(error));
307
- },
268
+ onError: (error) => toast.error(cancelErrorMessage(error)),
308
269
  });
309
270
  }
310
271
 
311
272
  const fatalError = query.isError && runData === undefined;
312
- const tabState = fatalError ? (
273
+ const loadingOrError = fatalError ? (
313
274
  query.error instanceof ApiError && query.error.status === 404 ? (
314
275
  <WorkflowRunNotFound />
315
276
  ) : (
@@ -320,146 +281,312 @@ function RunViewContent({
320
281
  ) : null;
321
282
 
322
283
  return (
323
- <>
324
- <Tabs
325
- value={activeTab}
326
- onValueChange={handleTabChange}
327
- className="flex min-w-0 flex-1 flex-col gap-0"
284
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col">
285
+ {runData ? (
286
+ <WorkflowRunSummary
287
+ workspaceSlug={workspaceSlug}
288
+ projectSlug={projectSlug}
289
+ run={runData}
290
+ cancelling={cancelMutation.isPending}
291
+ onCancel={cancelRun}
292
+ rerunPending={rerunMutation.isPending}
293
+ onRerun={(mode) => void rerun(mode)}
294
+ latestAttempt={runData.latestAttempt}
295
+ />
296
+ ) : (
297
+ <WorkflowRunSkeleton />
298
+ )}
299
+ {runData && query.isError ? <WorkflowRunStaleError query={query} /> : null}
300
+
301
+ <div
302
+ data-run-workspace-layout
303
+ className="flex min-h-0 min-w-0 flex-1 flex-col border-t border-border-neutral-base min-[768px]:flex-row"
328
304
  >
329
- {runData ? (
330
- <WorkflowRunSummary
305
+ {runData && workspaceSlug && projectSlug ? (
306
+ <RunWorkspaceNav
331
307
  workspaceSlug={workspaceSlug}
332
308
  projectSlug={projectSlug}
333
309
  run={runData}
334
- cancelling={cancelMutation.isPending}
335
- onCancel={cancelRun}
336
- rerunPending={rerunMutation.isPending}
337
- onRerun={(mode) => void rerun(mode)}
338
- latestAttempt={runData.latestAttempt}
310
+ activeSection={activeSection}
311
+ currentJobId={activeJobId}
312
+ jobSearch={jobSearch}
313
+ annotationSummary={annotationSummary}
339
314
  />
340
315
  ) : (
341
- <WorkflowRunSkeleton />
316
+ <RunWorkspaceNavSkeleton />
342
317
  )}
343
- {runData && query.isError ? <WorkflowRunStaleError query={query} /> : null}
344
- <RunTabStrip
345
- jobCount={runData?.jobs.length}
346
- jobsFailed={runData?.jobs.filter((job) => job.status === 'failed').length}
347
- annotationSummary={annotationSummary}
348
- />
349
- <TabsContents className="min-h-0 flex-1 overflow-auto bg-background-neutral-background pb-24 pt-16">
350
- <TabsContent
351
- value="summary"
352
- tabIndex={-1}
353
- className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24 outline-none"
354
- >
355
- {runData ? (
356
- <JobGraph
357
- run={runData}
358
- selectedJobId={selectedJob?.id}
359
- onSelectedJobChange={selectJob}
360
- />
361
- ) : (
362
- tabState
363
- )}
364
- </TabsContent>
365
- <TabsContent
366
- value="jobs"
367
- tabIndex={-1}
368
- className="mx-auto w-full max-w-[1120px] px-24 outline-none"
369
- >
370
- {runData ? (
371
- <RunJobsList
372
- jobs={runData.jobs}
373
- selectedJobId={selectedJob?.id}
374
- onSelectedJobChange={selectJob}
375
- workspaceSlug={workspaceSlug}
376
- selectedJobExecution={selectedJobExecution}
377
- selectedAttemptId={selectedAttemptId}
378
- onSelectedJobExecutionChange={selectJobExecution}
379
- onSelectedAttemptChange={selectionControlled ? selectAttempt : undefined}
380
- sourcePanelId={sourcePanelId}
381
- sourceAvailable={sourceAvailable}
382
- focusedSourceStepId={sourceFocus?.stepId ?? null}
383
- onOpenStepSource={openStepSource}
384
- />
385
- ) : (
386
- tabState
387
- )}
388
- </TabsContent>
389
- <TabsContent
390
- value="annotations"
391
- tabIndex={-1}
392
- className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24 outline-none"
393
- >
394
- {runData ? (
395
- <>
396
- <RunAnnotationSummaryLine
397
- summary={annotationSummary}
398
- workspaceSlug={workspaceSlug}
399
- projectSlug={projectSlug}
400
- workflowRunId={runData.id}
401
- search={selection}
402
- />
403
- <RunAnnotationsEmpty />
404
- </>
405
- ) : (
406
- tabState
407
- )}
408
- </TabsContent>
409
- <TabsContent
410
- value="source"
411
- keepMounted={sourceSnapshot !== null}
412
- tabIndex={-1}
413
- className="mx-auto flex min-h-full w-full max-w-[1120px] px-24 outline-none"
414
- >
415
- {runData ? (
416
- sourceSnapshot ? (
417
- <WorkflowSourceContent
418
- source={sourceSnapshot}
419
- highlightedLineRange={highlightedLineRange}
420
- scrollHighlightedIntoView
421
- />
422
- ) : (
423
- <EmptyState
424
- className="min-h-160 w-full"
425
- icon="fileDamageLine"
426
- title="Source snapshot unavailable"
427
- description={
428
- runData.isTemporary
429
- ? 'Temporary runs do not capture workflow source.'
430
- : 'This run was created before workflow source snapshots were captured.'
431
- }
432
- />
433
- )
434
- ) : (
435
- tabState
436
- )}
437
- </TabsContent>
438
- </TabsContents>
439
- </Tabs>
440
- <WorkflowSourcePanel
441
- id={sourcePanelId}
442
- source={sourceSnapshot}
443
- open={sourcePanelOpen && sourceAvailable}
444
- onClose={closeSourcePanel}
445
- highlightedLineRange={highlightedLineRange}
446
- scrollHighlightedIntoView
318
+
319
+ <div
320
+ data-run-workspace-content
321
+ className="flex min-h-0 min-w-0 flex-1 flex-col bg-background-neutral-base"
322
+ >
323
+ {runData && jobContent ? (
324
+ jobContent
325
+ ) : runData ? (
326
+ <RunSectionContent
327
+ section={activeSection}
328
+ run={runData}
329
+ annotations={annotations}
330
+ annotationSummary={annotationSummary}
331
+ workspaceSlug={workspaceSlug}
332
+ projectSlug={projectSlug}
333
+ selection={selection}
334
+ selectedJobId={selection?.jobId}
335
+ onSelectGraphJob={selectGraphJob}
336
+ onSelectAnnotationJob={selectAnnotationJob}
337
+ onClearAnnotationFilters={clearAnnotationFilters}
338
+ sourceSnapshot={sourceSnapshot}
339
+ highlightedLineRange={highlightedLineRange}
340
+ />
341
+ ) : (
342
+ <div className="min-h-0 flex-1 overflow-auto p-24">{loadingOrError}</div>
343
+ )}
344
+ </div>
345
+ </div>
346
+ </div>
347
+ );
348
+ }
349
+
350
+ function RunSectionContent({
351
+ section,
352
+ run,
353
+ annotations,
354
+ annotationSummary,
355
+ workspaceSlug,
356
+ projectSlug,
357
+ selection,
358
+ selectedJobId,
359
+ onSelectGraphJob,
360
+ onSelectAnnotationJob,
361
+ onClearAnnotationFilters,
362
+ sourceSnapshot,
363
+ highlightedLineRange,
364
+ }: {
365
+ section: RunWorkspaceSection;
366
+ run: NonNullable<ReturnType<typeof useWorkflowRunAttemptQuery>['data']>;
367
+ annotations: ReturnType<typeof useRunAnnotationsQuery>;
368
+ annotationSummary: RunAnnotationSummary | undefined;
369
+ workspaceSlug: string | undefined;
370
+ projectSlug: string | undefined;
371
+ selection: WorkflowRunsSearch | undefined;
372
+ selectedJobId: string | undefined;
373
+ onSelectGraphJob: (jobId: string | undefined, source?: JobGraphSelectionSource) => void;
374
+ onSelectAnnotationJob: (jobId: string | undefined) => void;
375
+ onClearAnnotationFilters: () => void;
376
+ sourceSnapshot: NonNullable<
377
+ ReturnType<typeof useWorkflowRunAttemptQuery>['data']
378
+ >['sourceSnapshot'];
379
+ highlightedLineRange: NonNullable<
380
+ ReturnType<typeof useWorkflowRunAttemptQuery>['data']
381
+ >['jobs'][number]['jobExecutions'][number]['steps'][number]['sourceLocation'];
382
+ }) {
383
+ if (section === 'summary') {
384
+ return (
385
+ <section aria-label="All jobs summary" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
386
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
387
+ <Text as="h2" className="sr-only">
388
+ All jobs summary
389
+ </Text>
390
+ <JobGraph
391
+ run={run}
392
+ selectedJobId={selectedJobId}
393
+ onSelectedJobChange={onSelectGraphJob}
394
+ className="min-h-160 overflow-hidden"
395
+ />
396
+ </div>
397
+ </section>
398
+ );
399
+ }
400
+
401
+ if (section === 'annotations') {
402
+ return (
403
+ <RunAnnotationsSection
404
+ run={run}
405
+ annotations={annotations}
406
+ annotationSummary={annotationSummary}
407
+ workspaceSlug={workspaceSlug}
408
+ projectSlug={projectSlug}
409
+ selection={selection}
410
+ selectedJobId={selectedJobId}
411
+ onSelectAnnotationJob={onSelectAnnotationJob}
412
+ onClearAnnotationFilters={onClearAnnotationFilters}
447
413
  />
448
- </>
414
+ );
415
+ }
416
+
417
+ return (
418
+ <section aria-label="Workflow source" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
419
+ <div className="mx-auto flex min-h-full w-full max-w-[1120px] flex-col px-24">
420
+ <Text as="h2" className="sr-only">
421
+ Workflow source
422
+ </Text>
423
+ {sourceSnapshot ? (
424
+ <WorkflowSourceContent
425
+ source={sourceSnapshot}
426
+ highlightedLineRange={highlightedLineRange}
427
+ scrollHighlightedIntoView
428
+ />
429
+ ) : (
430
+ <EmptyState
431
+ className="min-h-160 w-full"
432
+ icon="fileDamageLine"
433
+ title="Source snapshot unavailable"
434
+ description={
435
+ run.isTemporary
436
+ ? 'Temporary runs do not capture workflow source.'
437
+ : 'This run was created before workflow source snapshots were captured.'
438
+ }
439
+ />
440
+ )}
441
+ </div>
442
+ </section>
449
443
  );
450
444
  }
451
445
 
446
+ function RunAnnotationsSection({
447
+ run,
448
+ annotations,
449
+ annotationSummary,
450
+ workspaceSlug,
451
+ projectSlug,
452
+ selection,
453
+ selectedJobId,
454
+ onSelectAnnotationJob,
455
+ onClearAnnotationFilters,
456
+ }: {
457
+ run: NonNullable<ReturnType<typeof useWorkflowRunAttemptQuery>['data']>;
458
+ annotations: ReturnType<typeof useRunAnnotationsQuery>;
459
+ annotationSummary: RunAnnotationSummary | undefined;
460
+ workspaceSlug: string | undefined;
461
+ projectSlug: string | undefined;
462
+ selection: WorkflowRunsSearch | undefined;
463
+ selectedJobId: string | undefined;
464
+ onSelectAnnotationJob: (jobId: string | undefined) => void;
465
+ onClearAnnotationFilters: () => void;
466
+ }) {
467
+ const selectedJob = run.jobs.find((job) => job.id === selectedJobId);
468
+ const severity = selection?.severity;
469
+ const records = annotations.annotations;
470
+
471
+ const entries = useMemo(
472
+ () =>
473
+ records
474
+ ? buildRunAnnotationList({
475
+ annotations: records,
476
+ jobs: run.jobs,
477
+ severity,
478
+ jobId: selectedJob?.id,
479
+ })
480
+ : undefined,
481
+ [records, run.jobs, selectedJob?.id, severity],
482
+ );
483
+
484
+ return (
485
+ <section aria-label="Run annotations" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
486
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
487
+ <Text as="h2" className="sr-only">
488
+ Annotations
489
+ </Text>
490
+ <div className="flex flex-wrap items-center justify-between gap-8">
491
+ <RunAnnotationSummaryLine
492
+ summary={annotationSummary}
493
+ workspaceSlug={workspaceSlug}
494
+ projectSlug={projectSlug}
495
+ workflowRunId={run.id}
496
+ search={selection}
497
+ />
498
+ <AnnotationJobFilter
499
+ jobs={run.jobs}
500
+ selectedJobId={selectedJob?.id}
501
+ onSelect={onSelectAnnotationJob}
502
+ />
503
+ </div>
504
+ <RunAnnotationList
505
+ // Remounting on a filter or run-attempt change resets the render window, so the next
506
+ // list never inherits a "show more" position from different data.
507
+ key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
508
+ query={annotations.query}
509
+ entries={entries}
510
+ workspaceSlug={workspaceSlug}
511
+ projectSlug={projectSlug}
512
+ workflowRunId={run.id}
513
+ runAttempt={run.runAttempt.attempt}
514
+ // A run with no annotations at all offers no filter to clear, whatever the URL says.
515
+ filtered={Boolean((severity || selectedJob) && (annotationSummary?.total ?? 0) > 0)}
516
+ filteredJobName={selectedJob?.displayName}
517
+ filteredSeverity={severity}
518
+ onClearFilters={onClearAnnotationFilters}
519
+ selectedAnnotationId={selection?.annotation}
520
+ />
521
+ </div>
522
+ </section>
523
+ );
524
+ }
525
+
526
+ const ALL_ANNOTATION_JOBS = 'all-jobs';
527
+
528
+ function AnnotationJobFilter({
529
+ jobs,
530
+ selectedJobId,
531
+ onSelect,
532
+ }: {
533
+ jobs: Job[];
534
+ selectedJobId: string | undefined;
535
+ onSelect: (jobId: string | undefined) => void;
536
+ }) {
537
+ const sortedJobs = [...jobs].sort(
538
+ (left, right) => left.position - right.position || left.id.localeCompare(right.id),
539
+ );
540
+
541
+ return (
542
+ <Select
543
+ value={selectedJobId ?? ALL_ANNOTATION_JOBS}
544
+ onValueChange={(value) => onSelect(value === ALL_ANNOTATION_JOBS ? undefined : value)}
545
+ >
546
+ <SelectTrigger
547
+ size="small"
548
+ aria-label="Filter annotations by job"
549
+ className="w-full min-[480px]:w-240"
550
+ >
551
+ <SelectValue />
552
+ </SelectTrigger>
553
+ <SelectContent align="end">
554
+ <SelectItem value={ALL_ANNOTATION_JOBS}>All jobs</SelectItem>
555
+ {sortedJobs.map((job) => (
556
+ <SelectItem key={job.id} value={job.id}>
557
+ {job.displayName}
558
+ </SelectItem>
559
+ ))}
560
+ </SelectContent>
561
+ </Select>
562
+ );
563
+ }
564
+
565
+ function RunWorkspaceNavSkeleton() {
566
+ return (
567
+ <aside
568
+ aria-label="Loading run navigation"
569
+ className="hidden w-240 shrink-0 border-r border-border-neutral-base bg-background-neutral-background p-12 min-[768px]:block"
570
+ >
571
+ <div className="flex flex-col gap-16">
572
+ <div className="h-32 rounded-4 bg-background-components-subtle" />
573
+ <div className="flex flex-col gap-6">
574
+ <div className="h-16 w-64 rounded-4 bg-background-components-subtle" />
575
+ <div className="h-32 rounded-4 bg-background-components-subtle" />
576
+ <div className="h-32 rounded-4 bg-background-components-subtle" />
577
+ </div>
578
+ </div>
579
+ </aside>
580
+ );
581
+ }
582
+
583
+ function runWorkspaceSection(tab: WorkflowRunTab | undefined): RunWorkspaceSection {
584
+ return tab === 'annotations' || tab === 'source' ? tab : 'summary';
585
+ }
586
+
452
587
  function cancelErrorMessage(error: unknown): string {
453
588
  if (error instanceof ApiError && error.code === 'run-already-finished') {
454
589
  return 'This workflow run has already finished.';
455
590
  }
456
591
  return 'Could not cancel workflow run.';
457
592
  }
458
-
459
- function findAttemptSelection(jobExecution: JobExecution, attemptId: string) {
460
- for (const step of jobExecution.steps) {
461
- const attempt = step.attempts.find((candidate) => candidate.id === attemptId);
462
- if (attempt) return {stepId: step.id, attemptId: attempt.id};
463
- }
464
- return undefined;
465
- }