@shipfox/client-workflows 15.0.0 → 17.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 (225) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +41 -0
  5. package/dist/components/identifier/identifier.js +2 -2
  6. package/dist/components/identifier/identifier.js.map +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  8. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  9. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  10. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  11. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  12. package/dist/components/job-detail/job-context-panel.js +224 -0
  13. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  14. package/dist/components/job-detail/job-detail-header.d.ts +10 -1
  15. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  16. package/dist/components/job-detail/job-detail-header.js +58 -39
  17. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  19. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  20. package/dist/components/job-detail/job-detail-view.js +92 -41
  21. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  23. package/dist/components/job-detail/job-empty-states.js +19 -10
  24. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  25. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  26. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  27. package/dist/components/job-detail/json-code.d.ts +14 -0
  28. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  29. package/dist/components/job-detail/json-code.js +93 -0
  30. package/dist/components/job-detail/json-code.js.map +1 -0
  31. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  32. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  33. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  34. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  35. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  36. package/dist/components/job-detail/step-troubleshooting.js +493 -0
  37. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  39. package/dist/components/job-graph/job-graph-content.js +5 -4
  40. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  41. package/dist/components/job-graph/job-node.js +3 -3
  42. package/dist/components/job-graph/job-node.js.map +1 -1
  43. package/dist/components/step-list/index.d.ts +1 -0
  44. package/dist/components/step-list/index.d.ts.map +1 -1
  45. package/dist/components/step-list/index.js.map +1 -1
  46. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  47. package/dist/components/step-list/step-list-model.js +5 -0
  48. package/dist/components/step-list/step-list-model.js.map +1 -1
  49. package/dist/components/step-list/step-list.d.ts +5 -1
  50. package/dist/components/step-list/step-list.d.ts.map +1 -1
  51. package/dist/components/step-list/step-list.js +117 -82
  52. package/dist/components/step-list/step-list.js.map +1 -1
  53. package/dist/components/workflow-run-duration-label.js +1 -1
  54. package/dist/components/workflow-run-duration-label.js.map +1 -1
  55. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  56. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  60. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  62. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  66. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js +9 -13
  72. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts +3 -0
  74. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  75. package/dist/components/workflow-run-tabs/index.js +3 -0
  76. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  78. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  79. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  80. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  81. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  82. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  83. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  84. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  85. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +39 -0
  86. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-list.js +211 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  91. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +90 -25
  92. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  93. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  94. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  95. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  96. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  97. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
  98. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  99. package/dist/components/workflow-run-view/run-workspace-nav.js +16 -10
  100. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  101. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  102. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  103. package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
  104. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  105. package/dist/components/workflow-run-view/workflow-run-view.js +217 -59
  106. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  107. package/dist/core/entities/job-execution.d.ts +7 -0
  108. package/dist/core/entities/job-execution.d.ts.map +1 -1
  109. package/dist/core/entities/job-execution.js.map +1 -1
  110. package/dist/core/entities/job.d.ts +9 -0
  111. package/dist/core/entities/job.d.ts.map +1 -1
  112. package/dist/core/entities/job.js.map +1 -1
  113. package/dist/core/entities/step-attempt.d.ts +29 -0
  114. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  115. package/dist/core/entities/step-attempt.js.map +1 -1
  116. package/dist/core/entities/step.d.ts +3 -1
  117. package/dist/core/entities/step.d.ts.map +1 -1
  118. package/dist/core/entities/step.js.map +1 -1
  119. package/dist/core/run-annotation.d.ts +79 -0
  120. package/dist/core/run-annotation.d.ts.map +1 -0
  121. package/dist/core/run-annotation.js +125 -0
  122. package/dist/core/run-annotation.js.map +1 -0
  123. package/dist/core/workflow-run.d.ts +1 -1
  124. package/dist/core/workflow-run.d.ts.map +1 -1
  125. package/dist/core/workflow-run.js.map +1 -1
  126. package/dist/hooks/api/annotations.d.ts +63 -0
  127. package/dist/hooks/api/annotations.d.ts.map +1 -0
  128. package/dist/hooks/api/annotations.js +145 -0
  129. package/dist/hooks/api/annotations.js.map +1 -0
  130. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  131. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  132. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  133. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  134. package/dist/hooks/api/run-annotations.d.ts +25 -0
  135. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  136. package/dist/hooks/api/run-annotations.js +78 -0
  137. package/dist/hooks/api/run-annotations.js.map +1 -0
  138. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  139. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  140. package/dist/hooks/api/step-attempt-detail.js +43 -0
  141. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  142. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  143. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  144. package/dist/hooks/api/workflow-run-mapper.js +47 -0
  145. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  146. package/dist/pages/project-workflows-page.js +19 -19
  147. package/dist/pages/project-workflows-page.js.map +1 -1
  148. package/dist/pages/workflow-run-list-page.js +1 -1
  149. package/dist/pages/workflow-run-list-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +7 -2
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +5 -6
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/tsconfig.test.tsbuildinfo +1 -1
  155. package/package.json +10 -9
  156. package/src/components/identifier/identifier.tsx +2 -2
  157. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  158. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  159. package/src/components/job-detail/job-context-panel.tsx +237 -0
  160. package/src/components/job-detail/job-detail-header.test.tsx +3 -0
  161. package/src/components/job-detail/job-detail-header.tsx +65 -34
  162. package/src/components/job-detail/job-detail-view.stories.tsx +555 -48
  163. package/src/components/job-detail/job-detail-view.tsx +101 -42
  164. package/src/components/job-detail/job-empty-states.tsx +23 -10
  165. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  166. package/src/components/job-detail/json-code.tsx +120 -0
  167. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  168. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  169. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  170. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  171. package/src/components/job-detail/step-troubleshooting.tsx +537 -0
  172. package/src/components/job-graph/job-graph-content.tsx +4 -3
  173. package/src/components/job-graph/job-node.tsx +3 -3
  174. package/src/components/step-list/index.ts +1 -0
  175. package/src/components/step-list/step-list-model.ts +5 -0
  176. package/src/components/step-list/step-list.test.tsx +75 -2
  177. package/src/components/step-list/step-list.tsx +73 -20
  178. package/src/components/workflow-run-duration-label.tsx +1 -1
  179. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  180. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  181. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  182. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  183. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  184. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  185. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  186. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  187. package/src/components/workflow-run-summary/workflow-run-summary.tsx +12 -12
  188. package/src/components/workflow-run-tabs/index.ts +7 -0
  189. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  190. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  191. package/src/components/workflow-run-tabs/run-annotation-list.tsx +273 -0
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  193. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +95 -20
  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/run-workspace-nav.test.tsx +44 -0
  197. package/src/components/workflow-run-view/run-workspace-nav.tsx +22 -9
  198. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  199. package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
  200. package/src/components/workflow-run-view/workflow-run-view.tsx +255 -37
  201. package/src/core/entities/job-execution.ts +7 -0
  202. package/src/core/entities/job.ts +9 -0
  203. package/src/core/entities/step-attempt.ts +33 -0
  204. package/src/core/entities/step.ts +3 -1
  205. package/src/core/run-annotation.test.ts +315 -0
  206. package/src/core/run-annotation.ts +244 -0
  207. package/src/core/workflow-run.ts +4 -0
  208. package/src/hooks/api/annotations.test.ts +84 -0
  209. package/src/hooks/api/annotations.ts +240 -0
  210. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  211. package/src/hooks/api/run-annotations.ts +137 -0
  212. package/src/hooks/api/step-attempt-detail.ts +67 -0
  213. package/src/hooks/api/workflow-run-mapper.ts +46 -0
  214. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  215. package/src/pages/project-workflows-page.tsx +19 -19
  216. package/src/pages/workflow-job-detail-page.test.tsx +118 -1
  217. package/src/pages/workflow-run-list-page.tsx +1 -1
  218. package/src/routes/inputs.ts +7 -2
  219. package/test/fixtures/workflow-run.ts +7 -0
  220. package/tsconfig.build.tsbuildinfo +1 -1
  221. package/dist/core/workflow-run-tabs.d.ts +0 -9
  222. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  223. package/dist/core/workflow-run-tabs.js +0 -3
  224. package/dist/core/workflow-run-tabs.js.map +0 -1
  225. package/src/core/workflow-run-tabs.ts +0 -8
@@ -0,0 +1,240 @@
1
+ import {
2
+ type AnnotationStyleDto,
3
+ annotationSummaryResponseSchema,
4
+ readAnnotationsResponseSchema,
5
+ } from '@shipfox/annotations-dto';
6
+ import {checkedApiRequest} from '@shipfox/client-api';
7
+ import {
8
+ infiniteQueryOptions,
9
+ queryOptions,
10
+ useInfiniteQuery,
11
+ useQuery,
12
+ } from '@tanstack/react-query';
13
+ import type {RunAnnotationSummary} from '#core/run-annotation.js';
14
+
15
+ const ANNOTATIONS_PAGE_SIZE = 100;
16
+ const ANNOTATIONS_REFRESH_INTERVAL_MS = 5_000;
17
+
18
+ export interface WorkflowRunAnnotation {
19
+ id: string;
20
+ jobId: string;
21
+ jobExecutionId: string;
22
+ originStepId: string;
23
+ originStepAttempt: number;
24
+ context: string;
25
+ style: AnnotationStyleDto;
26
+ sequence: number;
27
+ body: string;
28
+ }
29
+
30
+ export interface WorkflowRunAnnotationsPage {
31
+ annotations: WorkflowRunAnnotation[];
32
+ hasMore: boolean;
33
+ nextCursor: string | null;
34
+ }
35
+
36
+ export const workflowRunAnnotationsQueryKeys = {
37
+ all: ['workflow-run-annotations'] as const,
38
+ list: (workflowRunId: string, attempt: number, jobExecutionId?: string) =>
39
+ [
40
+ ...workflowRunAnnotationsQueryKeys.all,
41
+ 'list',
42
+ workflowRunId,
43
+ attempt,
44
+ jobExecutionId ?? null,
45
+ ] as const,
46
+ summary: (workflowRunId: string, attempt: number, jobExecutionId?: string) =>
47
+ [
48
+ ...workflowRunAnnotationsQueryKeys.all,
49
+ 'summary',
50
+ workflowRunId,
51
+ attempt,
52
+ jobExecutionId ?? null,
53
+ ] as const,
54
+ };
55
+
56
+ function annotationParams({
57
+ workflowRunId,
58
+ attempt,
59
+ jobExecutionId,
60
+ cursor,
61
+ }: {
62
+ workflowRunId: string;
63
+ attempt: number;
64
+ jobExecutionId?: string | undefined;
65
+ cursor?: string | null | undefined;
66
+ }): URLSearchParams {
67
+ const params = new URLSearchParams({
68
+ workflow_run_id: workflowRunId,
69
+ attempt: String(attempt),
70
+ limit: String(ANNOTATIONS_PAGE_SIZE),
71
+ });
72
+ if (jobExecutionId) params.set('job_execution_id', jobExecutionId);
73
+ if (cursor) params.set('cursor', cursor);
74
+ return params;
75
+ }
76
+
77
+ async function getWorkflowRunAnnotationsPage({
78
+ workflowRunId,
79
+ attempt,
80
+ jobExecutionId,
81
+ cursor,
82
+ signal,
83
+ }: {
84
+ workflowRunId: string;
85
+ attempt: number;
86
+ jobExecutionId?: string | undefined;
87
+ cursor?: string | null | undefined;
88
+ signal?: AbortSignal;
89
+ }): Promise<WorkflowRunAnnotationsPage> {
90
+ const response = await checkedApiRequest(
91
+ readAnnotationsResponseSchema,
92
+ `/annotations?${annotationParams({workflowRunId, attempt, jobExecutionId, cursor}).toString()}`,
93
+ {signal},
94
+ );
95
+ return {
96
+ annotations: response.annotations.map(toWorkflowRunAnnotation),
97
+ hasMore: response.has_more,
98
+ nextCursor: response.next_cursor,
99
+ };
100
+ }
101
+
102
+ async function getWorkflowRunAnnotationSummary({
103
+ workflowRunId,
104
+ attempt,
105
+ jobExecutionId,
106
+ signal,
107
+ }: {
108
+ workflowRunId: string;
109
+ attempt: number;
110
+ jobExecutionId?: string | undefined;
111
+ signal?: AbortSignal;
112
+ }): Promise<RunAnnotationSummary> {
113
+ const params = annotationParams({workflowRunId, attempt, jobExecutionId});
114
+ params.delete('limit');
115
+ const response = await checkedApiRequest(
116
+ annotationSummaryResponseSchema,
117
+ `/annotations/summary?${params.toString()}`,
118
+ {signal},
119
+ );
120
+ return {
121
+ total: response.total,
122
+ error: response.error,
123
+ warning: response.warning,
124
+ info: response.info,
125
+ success: response.success,
126
+ truncated: false,
127
+ stepCounts: response.step_counts.map((step) => ({
128
+ stepId: step.origin_step_id,
129
+ attempt: step.origin_step_attempt,
130
+ total: step.total,
131
+ })),
132
+ };
133
+ }
134
+
135
+ export function workflowRunAnnotationsQueryOptions(
136
+ workflowRunId: string | undefined,
137
+ attempt: number | undefined,
138
+ jobExecutionId?: string | undefined,
139
+ options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
140
+ ) {
141
+ const enabled = Boolean(workflowRunId) && attempt !== undefined && (options?.enabled ?? true);
142
+ const polling = options?.polling ?? true;
143
+ return infiniteQueryOptions({
144
+ queryKey:
145
+ workflowRunId && attempt !== undefined
146
+ ? workflowRunAnnotationsQueryKeys.list(workflowRunId, attempt, jobExecutionId)
147
+ : ([...workflowRunAnnotationsQueryKeys.all, 'list'] as const),
148
+ enabled,
149
+ initialPageParam: null as string | null,
150
+ queryFn: ({pageParam, signal}) =>
151
+ getWorkflowRunAnnotationsPage({
152
+ workflowRunId: workflowRunId ?? '',
153
+ attempt: attempt ?? 0,
154
+ ...(jobExecutionId ? {jobExecutionId} : {}),
155
+ cursor: pageParam,
156
+ signal,
157
+ }),
158
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
159
+ staleTime: 2_000,
160
+ refetchInterval:
161
+ enabled && polling
162
+ ? (query) => {
163
+ const pages = query.state.data?.pages;
164
+ return pages && pages.length > 1 ? false : ANNOTATIONS_REFRESH_INTERVAL_MS;
165
+ }
166
+ : false,
167
+ });
168
+ }
169
+
170
+ export function useWorkflowRunAnnotationsQuery(
171
+ workflowRunId: string | undefined,
172
+ attempt: number | undefined,
173
+ jobExecutionId?: string | undefined,
174
+ options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
175
+ ) {
176
+ return useInfiniteQuery(
177
+ workflowRunAnnotationsQueryOptions(workflowRunId, attempt, jobExecutionId, options),
178
+ );
179
+ }
180
+
181
+ export function workflowRunAnnotationSummaryQueryOptions(
182
+ workflowRunId: string | undefined,
183
+ attempt: number | undefined,
184
+ jobExecutionId?: string | undefined,
185
+ options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
186
+ ) {
187
+ const enabled = Boolean(workflowRunId) && attempt !== undefined && (options?.enabled ?? true);
188
+ const polling = options?.polling ?? true;
189
+ return queryOptions({
190
+ queryKey:
191
+ workflowRunId && attempt !== undefined
192
+ ? workflowRunAnnotationsQueryKeys.summary(workflowRunId, attempt, jobExecutionId)
193
+ : ([...workflowRunAnnotationsQueryKeys.all, 'summary'] as const),
194
+ enabled,
195
+ queryFn: ({signal}) =>
196
+ getWorkflowRunAnnotationSummary({
197
+ workflowRunId: workflowRunId ?? '',
198
+ attempt: attempt ?? 0,
199
+ ...(jobExecutionId ? {jobExecutionId} : {}),
200
+ signal,
201
+ }),
202
+ staleTime: 2_000,
203
+ refetchInterval: enabled && polling ? ANNOTATIONS_REFRESH_INTERVAL_MS : false,
204
+ });
205
+ }
206
+
207
+ export function useWorkflowRunAnnotationSummaryQuery(
208
+ workflowRunId: string | undefined,
209
+ attempt: number | undefined,
210
+ jobExecutionId?: string | undefined,
211
+ options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
212
+ ) {
213
+ return useQuery(
214
+ workflowRunAnnotationSummaryQueryOptions(workflowRunId, attempt, jobExecutionId, options),
215
+ );
216
+ }
217
+
218
+ export function flattenWorkflowRunAnnotations(
219
+ data: {pages: readonly WorkflowRunAnnotationsPage[]} | undefined,
220
+ ): WorkflowRunAnnotation[] {
221
+ return data?.pages.flatMap((page) => page.annotations) ?? [];
222
+ }
223
+
224
+ function toWorkflowRunAnnotation(
225
+ annotation: Awaited<
226
+ ReturnType<typeof readAnnotationsResponseSchema.parse>
227
+ >['annotations'][number],
228
+ ): WorkflowRunAnnotation {
229
+ return {
230
+ id: annotation.id,
231
+ jobId: annotation.job_id,
232
+ jobExecutionId: annotation.job_execution_id,
233
+ originStepId: annotation.origin_step_id,
234
+ originStepAttempt: annotation.origin_step_attempt,
235
+ context: annotation.context,
236
+ style: annotation.style,
237
+ sequence: annotation.sequence,
238
+ body: annotation.body,
239
+ };
240
+ }
@@ -0,0 +1,30 @@
1
+ import type {AnnotationDto, ReadAnnotationsResponseDto} from '@shipfox/annotations-dto';
2
+ import type {RunAnnotationRecord} from '#core/run-annotation.js';
3
+
4
+ export interface RunAnnotationPage {
5
+ annotations: RunAnnotationRecord[];
6
+ hasMore: boolean;
7
+ nextCursor: string | null;
8
+ }
9
+
10
+ export function toRunAnnotation(dto: AnnotationDto): RunAnnotationRecord {
11
+ return {
12
+ id: dto.id,
13
+ jobId: dto.job_id,
14
+ jobExecutionId: dto.job_execution_id,
15
+ originStepId: dto.origin_step_id,
16
+ originStepAttempt: dto.origin_step_attempt,
17
+ context: dto.context,
18
+ style: dto.style,
19
+ sequence: dto.sequence,
20
+ body: dto.body,
21
+ };
22
+ }
23
+
24
+ export function toRunAnnotationPage(response: ReadAnnotationsResponseDto): RunAnnotationPage {
25
+ return {
26
+ annotations: response.annotations.map(toRunAnnotation),
27
+ hasMore: response.has_more,
28
+ nextCursor: response.next_cursor,
29
+ };
30
+ }
@@ -0,0 +1,137 @@
1
+ import {READ_ANNOTATIONS_MAX_LIMIT, readAnnotationsResponseSchema} from '@shipfox/annotations-dto';
2
+ import {checkedApiRequest} from '@shipfox/client-api';
3
+ import {
4
+ type InfiniteData,
5
+ infiniteQueryOptions,
6
+ type UseInfiniteQueryOptions,
7
+ useInfiniteQuery,
8
+ } from '@tanstack/react-query';
9
+ import {useMemo} from 'react';
10
+ import {
11
+ type RunAnnotationRecord,
12
+ type RunAnnotationSummary,
13
+ summarizeRunAnnotations,
14
+ } from '#core/run-annotation.js';
15
+ import {type RunAnnotationPage, toRunAnnotationPage} from './run-annotation-mapper.js';
16
+
17
+ /** Matches the run detail poll, so annotations and run state never disagree by more than a tick. */
18
+ const ACTIVE_POLL_MS = 4_000;
19
+
20
+ export const runAnnotationsQueryKeys = {
21
+ all: ['run-annotations'] as const,
22
+ list: (workflowRunId: string, runAttempt?: number | undefined) =>
23
+ [...runAnnotationsQueryKeys.all, 'list', workflowRunId, runAttempt ?? null] as const,
24
+ };
25
+
26
+ type RunAnnotationsQueryKey =
27
+ | ReturnType<typeof runAnnotationsQueryKeys.list>
28
+ | readonly ['run-annotations', 'list'];
29
+
30
+ type RunAnnotationsQueryOptions = UseInfiniteQueryOptions<
31
+ RunAnnotationPage,
32
+ Error,
33
+ InfiniteData<RunAnnotationPage, string | undefined>,
34
+ RunAnnotationsQueryKey,
35
+ string | undefined
36
+ >;
37
+
38
+ async function listRunAnnotations({
39
+ workflowRunId,
40
+ runAttempt,
41
+ cursor,
42
+ signal,
43
+ }: {
44
+ workflowRunId: string;
45
+ runAttempt: number;
46
+ cursor?: string | undefined;
47
+ signal?: AbortSignal;
48
+ }): Promise<RunAnnotationPage> {
49
+ const params = new URLSearchParams({
50
+ workflow_run_id: workflowRunId,
51
+ attempt: String(runAttempt),
52
+ limit: String(READ_ANNOTATIONS_MAX_LIMIT),
53
+ });
54
+ if (cursor) params.set('cursor', cursor);
55
+
56
+ return toRunAnnotationPage(
57
+ await checkedApiRequest(readAnnotationsResponseSchema, `/annotations?${params.toString()}`, {
58
+ signal,
59
+ }),
60
+ );
61
+ }
62
+
63
+ export interface RunAnnotationsQueryInput {
64
+ workflowRunId: string | undefined;
65
+ runAttempt: number | undefined;
66
+ /** Poll while the run attempt is non-terminal, then settle. */
67
+ live?: boolean | undefined;
68
+ }
69
+
70
+ export function runAnnotationsQueryOptions({
71
+ workflowRunId,
72
+ runAttempt,
73
+ live = false,
74
+ }: RunAnnotationsQueryInput): RunAnnotationsQueryOptions {
75
+ const enabled = Boolean(workflowRunId) && runAttempt !== undefined;
76
+
77
+ // Polling stops once the reader has paged past the first page: the cursor bounding page 2 was
78
+ // computed from page 1's last row, so a refetch that shifts that boundary can drop a range of
79
+ // annotations into a between-pages gap.
80
+ return infiniteQueryOptions({
81
+ queryKey: workflowRunId
82
+ ? runAnnotationsQueryKeys.list(workflowRunId, runAttempt)
83
+ : ([...runAnnotationsQueryKeys.all, 'list'] as const),
84
+ enabled,
85
+ initialPageParam: undefined as string | undefined,
86
+ queryFn: ({pageParam, signal}) =>
87
+ listRunAnnotations({
88
+ workflowRunId: workflowRunId ?? '',
89
+ runAttempt: runAttempt ?? 1,
90
+ cursor: pageParam,
91
+ signal,
92
+ }),
93
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
94
+ staleTime: 2_000,
95
+ refetchOnWindowFocus: true,
96
+ refetchInterval: (query) => {
97
+ if (!live) return false;
98
+ const pages = query.state.data?.pages;
99
+ return pages && pages.length > 1 ? false : ACTIVE_POLL_MS;
100
+ },
101
+ refetchIntervalInBackground: false,
102
+ });
103
+ }
104
+
105
+ export interface RunAnnotationsQueryResult {
106
+ query: ReturnType<
107
+ typeof useInfiniteQuery<
108
+ RunAnnotationPage,
109
+ Error,
110
+ InfiniteData<RunAnnotationPage, string | undefined>,
111
+ RunAnnotationsQueryKey,
112
+ string | undefined
113
+ >
114
+ >;
115
+ /** `undefined` until the first page resolves, so counts never render a speculative zero. */
116
+ annotations: RunAnnotationRecord[] | undefined;
117
+ summary: RunAnnotationSummary | undefined;
118
+ }
119
+
120
+ export function useRunAnnotationsQuery(input: RunAnnotationsQueryInput): RunAnnotationsQueryResult {
121
+ const query = useInfiniteQuery(runAnnotationsQueryOptions(input));
122
+ const pages = query.data?.pages;
123
+
124
+ const annotations = useMemo(
125
+ () => (pages ? pages.flatMap((page) => page.annotations) : undefined),
126
+ [pages],
127
+ );
128
+ const summary = useMemo(
129
+ () =>
130
+ annotations
131
+ ? summarizeRunAnnotations(annotations, {truncated: pages?.at(-1)?.hasMore ?? false})
132
+ : undefined,
133
+ [annotations, pages],
134
+ );
135
+
136
+ return {query, annotations, summary};
137
+ }
@@ -0,0 +1,67 @@
1
+ import {stepAttemptDetailResponseSchema} from '@shipfox/api-workflows-dto';
2
+ import {checkedApiRequest} from '@shipfox/client-api';
3
+ import {queryOptions, type UseQueryOptions, useQuery} from '@tanstack/react-query';
4
+ import type {StepAttemptDetail} from '#core/workflow-run.js';
5
+ import {toStepAttemptDetail} from './workflow-run-mapper.js';
6
+
7
+ export const stepAttemptDetailQueryKeys = {
8
+ all: ['workflow-step-attempt-details'] as const,
9
+ detail: (stepId: string, attempt: number) =>
10
+ [...stepAttemptDetailQueryKeys.all, stepId, attempt] as const,
11
+ };
12
+
13
+ type StepAttemptDetailQueryKey =
14
+ | ReturnType<typeof stepAttemptDetailQueryKeys.detail>
15
+ | readonly ['workflow-step-attempt-details'];
16
+
17
+ type StepAttemptDetailQueryOptions = UseQueryOptions<
18
+ StepAttemptDetail,
19
+ Error,
20
+ StepAttemptDetail,
21
+ StepAttemptDetailQueryKey
22
+ >;
23
+
24
+ async function getStepAttemptDetail({
25
+ stepId,
26
+ attempt,
27
+ signal,
28
+ }: {
29
+ stepId: string;
30
+ attempt: number;
31
+ signal?: AbortSignal;
32
+ }): Promise<StepAttemptDetail> {
33
+ const response = await checkedApiRequest(
34
+ stepAttemptDetailResponseSchema,
35
+ `/workflows/runs/steps/${stepId}/attempts/${attempt}`,
36
+ {signal},
37
+ );
38
+ return toStepAttemptDetail(response);
39
+ }
40
+
41
+ export function stepAttemptDetailQueryOptions(
42
+ stepId: string | undefined,
43
+ attempt: number | undefined,
44
+ ): StepAttemptDetailQueryOptions {
45
+ return queryOptions({
46
+ queryKey:
47
+ stepId && attempt !== undefined
48
+ ? stepAttemptDetailQueryKeys.detail(stepId, attempt)
49
+ : ([...stepAttemptDetailQueryKeys.all] as const),
50
+ enabled: Boolean(stepId) && attempt !== undefined,
51
+ queryFn: ({signal}) =>
52
+ getStepAttemptDetail({stepId: stepId ?? '', attempt: attempt ?? 0, signal}),
53
+ staleTime: Infinity,
54
+ });
55
+ }
56
+
57
+ export function useStepAttemptDetailQuery(
58
+ stepId: string | undefined,
59
+ attempt: number | undefined,
60
+ options?: {enabled?: boolean | undefined},
61
+ ) {
62
+ return useQuery({
63
+ ...stepAttemptDetailQueryOptions(stepId, attempt),
64
+ ...options,
65
+ enabled: Boolean(stepId) && attempt !== undefined && (options?.enabled ?? true),
66
+ });
67
+ }
@@ -1,5 +1,7 @@
1
1
  import type {
2
+ EvaluationTraceDto,
2
3
  JobListeningDto,
4
+ StepAttemptDetailResponseDto,
3
5
  StepAttemptDto,
4
6
  StepGateResultDto,
5
7
  WorkflowExecutionEventDto,
@@ -13,6 +15,7 @@ import type {
13
15
  WorkflowRunStepDetailDto,
14
16
  } from '@shipfox/api-workflows-dto';
15
17
  import {
18
+ type EvaluationTraceEntry,
16
19
  Job,
17
20
  JobExecution,
18
21
  type JobListening,
@@ -139,6 +142,10 @@ export function toJob(dto: WorkflowRunJobDetailDto): Job {
139
142
  status: dto.status,
140
143
  statusReason: dto.status_reason,
141
144
  carriedOver: dto.carried_over,
145
+ outputs: dto.outputs ?? null,
146
+ success: dto.success ?? null,
147
+ runner: dto.runner ?? null,
148
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
142
149
  listening: dto.listening ? toJobListening(dto.listening) : null,
143
150
  listenerStatus: dto.listener_status,
144
151
  resolutionReason: dto.resolution_reason,
@@ -158,11 +165,14 @@ export function toJobExecution(dto: WorkflowRunJobExecutionDetailDto): JobExecut
158
165
  name: dto.name,
159
166
  status: dto.status,
160
167
  statusReason: dto.status_reason,
168
+ runner: dto.runner ?? null,
169
+ outputs: dto.outputs ?? null,
161
170
  triggerEvents: (dto.trigger_events ?? []).map(toWorkflowExecutionEvent),
162
171
  queuedAt: dto.queued_at ?? null,
163
172
  startedAt: dto.started_at ?? null,
164
173
  finishedAt: dto.finished_at ?? null,
165
174
  timedOutAt: dto.timed_out_at ?? null,
175
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
166
176
  createdAt: dto.created_at,
167
177
  updatedAt: dto.updated_at,
168
178
  steps: dto.steps.map(toStep),
@@ -179,8 +189,10 @@ export function toStep(dto: WorkflowRunStepDetailDto): Step {
179
189
  ? {startLine: dto.source_location.start_line, endLine: dto.source_location.end_line}
180
190
  : null,
181
191
  status: dto.status,
192
+ statusReason: dto.status_reason,
182
193
  type: dto.type,
183
194
  config: dto.config,
195
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
184
196
  agentConfig: toAgentStepConfig(dto),
185
197
  error: dto.error
186
198
  ? {
@@ -210,6 +222,8 @@ export function toStepAttempt(dto: StepAttemptDto, jobExecutionId: string): Step
210
222
  status: dto.status,
211
223
  exitCode: dto.exit_code ?? null,
212
224
  output: dto.output ?? null,
225
+ outputs: dto.outputs ?? dto.output ?? null,
226
+ response: dto.response ?? null,
213
227
  error: dto.error ?? null,
214
228
  gateResult: toStepGateResult(dto.gate_result),
215
229
  restartFeedback: dto.restart_feedback ?? null,
@@ -218,6 +232,16 @@ export function toStepAttempt(dto: StepAttemptDto, jobExecutionId: string): Step
218
232
  });
219
233
  }
220
234
 
235
+ export function toStepAttemptDetail(dto: StepAttemptDetailResponseDto) {
236
+ return {
237
+ stepId: dto.step_id,
238
+ attempt: dto.attempt,
239
+ authoredConfig: dto.authored_config,
240
+ config: dto.config,
241
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
242
+ };
243
+ }
244
+
221
245
  function toJobListening(dto: JobListeningDto): JobListening {
222
246
  return {
223
247
  on: dto.on,
@@ -259,6 +283,28 @@ function toStepGateResult(dto: StepGateResultDto): StepGateResult {
259
283
  return dto;
260
284
  }
261
285
 
286
+ function toEvaluationTrace(trace: EvaluationTraceDto | null): EvaluationTraceEntry[] | null {
287
+ return (
288
+ trace?.map((entry) =>
289
+ 'dropped' in entry
290
+ ? entry
291
+ : {
292
+ expression: entry.expression,
293
+ roots: entry.roots,
294
+ fillTarget: entry.fill_target,
295
+ evaluatedAt: entry.evaluated_at,
296
+ field: entry.field,
297
+ ...(entry.value === undefined ? {} : {value: entry.value}),
298
+ ...(entry.truncated === undefined ? {} : {truncated: entry.truncated}),
299
+ ...(entry.expr_truncated === undefined ? {} : {exprTruncated: entry.expr_truncated}),
300
+ ...(entry.reference === undefined ? {} : {reference: entry.reference}),
301
+ ...(entry.degraded === undefined ? {} : {degraded: entry.degraded}),
302
+ ...(entry.env_key === undefined ? {} : {envKey: entry.env_key}),
303
+ },
304
+ ) ?? null
305
+ );
306
+ }
307
+
262
308
  function toAgentStepConfig(dto: WorkflowRunStepDetailDto): Step['agentConfig'] {
263
309
  if (dto.type !== 'agent') return null;
264
310
  return {
@@ -1,3 +1,4 @@
1
+ import type {StepAttemptDetailResponseDto} from '@shipfox/api-workflows-dto';
1
2
  import {configureApiClient} from '@shipfox/client-api';
2
3
  import {type InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
3
4
  import {act, cleanup, renderHook, waitFor} from '@testing-library/react';
@@ -11,7 +12,8 @@ import {
11
12
  workflowRunDto,
12
13
  workflowRunListResponseDto,
13
14
  } from '#test/fixtures/workflow-run.js';
14
- import {toWorkflowRun, toWorkflowRunListPage} from './workflow-run-mapper.js';
15
+ import {useStepAttemptDetailQuery} from './step-attempt-detail.js';
16
+ import {toStepAttemptDetail, toWorkflowRun, toWorkflowRunListPage} from './workflow-run-mapper.js';
15
17
  import {
16
18
  fireManualWorkflow,
17
19
  useCancelWorkflowRunMutation,
@@ -133,6 +135,64 @@ describe('workflow run API hooks', () => {
133
135
  expect(cached).toHaveProperty('triggerSource', 'manual');
134
136
  });
135
137
 
138
+ test('maps lazy step attempt details to authored and resolved troubleshooting data', () => {
139
+ const dto: StepAttemptDetailResponseDto = {
140
+ step_id: '88888888-8888-4888-8888-888888888888',
141
+ attempt: 2,
142
+ authored_config: {run: 'echo $' + '{{ inputs.message }}'},
143
+ config: {run: 'echo hello'},
144
+ evaluation_trace: [
145
+ {
146
+ expression: 'inputs.message',
147
+ roots: ['inputs.message'],
148
+ fill_target: 'run',
149
+ evaluated_at: '2026-08-05T12:00:00.000Z',
150
+ field: 'run',
151
+ value: 'hello',
152
+ degraded: false,
153
+ },
154
+ ],
155
+ };
156
+
157
+ expect(toStepAttemptDetail(dto)).toEqual({
158
+ stepId: dto.step_id,
159
+ attempt: 2,
160
+ authoredConfig: dto.authored_config,
161
+ config: dto.config,
162
+ evaluationTrace: [
163
+ {
164
+ expression: 'inputs.message',
165
+ roots: ['inputs.message'],
166
+ fillTarget: 'run',
167
+ evaluatedAt: '2026-08-05T12:00:00.000Z',
168
+ field: 'run',
169
+ value: 'hello',
170
+ degraded: false,
171
+ },
172
+ ],
173
+ });
174
+ });
175
+
176
+ test('does not fetch step attempt details while the inspector is closed', async () => {
177
+ const fetchImpl = vi.fn(async () =>
178
+ jsonResponse({
179
+ step_id: '88888888-8888-4888-8888-888888888888',
180
+ attempt: 1,
181
+ authored_config: null,
182
+ config: {},
183
+ evaluation_trace: null,
184
+ }),
185
+ );
186
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
187
+
188
+ renderWithQueryClient(() =>
189
+ useStepAttemptDetailQuery('88888888-8888-4888-8888-888888888888', 1, {enabled: false}),
190
+ );
191
+
192
+ await Promise.resolve();
193
+ expect(fetchImpl).not.toHaveBeenCalled();
194
+ });
195
+
136
196
  test('rejects malformed detail responses before they reach the query cache', async () => {
137
197
  const fetchImpl = vi.fn(async () => jsonResponse({id: RUN_ID, trigger_source: 'manual'}));
138
198
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});