@shipfox/client-workflows 16.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 (180) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-type.log +1 -0
  3. package/CHANGELOG.md +20 -0
  4. package/dist/components/identifier/identifier.js +2 -2
  5. package/dist/components/identifier/identifier.js.map +1 -1
  6. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  8. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  9. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  10. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  11. package/dist/components/job-detail/job-context-panel.js +224 -0
  12. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  13. package/dist/components/job-detail/job-detail-header.d.ts +3 -1
  14. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  15. package/dist/components/job-detail/job-detail-header.js +54 -48
  16. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  17. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  18. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  19. package/dist/components/job-detail/job-detail-view.js +80 -44
  20. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  21. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.js +19 -10
  23. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  24. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  25. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  26. package/dist/components/job-detail/json-code.d.ts +14 -0
  27. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  28. package/dist/components/job-detail/json-code.js +93 -0
  29. package/dist/components/job-detail/json-code.js.map +1 -0
  30. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  31. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  32. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  33. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  34. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  35. package/dist/components/job-detail/step-troubleshooting.js +493 -0
  36. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  37. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  38. package/dist/components/job-graph/job-graph-content.js +5 -4
  39. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  40. package/dist/components/job-graph/job-node.js +3 -3
  41. package/dist/components/job-graph/job-node.js.map +1 -1
  42. package/dist/components/step-list/index.d.ts +1 -0
  43. package/dist/components/step-list/index.d.ts.map +1 -1
  44. package/dist/components/step-list/index.js.map +1 -1
  45. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  46. package/dist/components/step-list/step-list-model.js +5 -0
  47. package/dist/components/step-list/step-list-model.js.map +1 -1
  48. package/dist/components/step-list/step-list.d.ts +5 -1
  49. package/dist/components/step-list/step-list.d.ts.map +1 -1
  50. package/dist/components/step-list/step-list.js +117 -82
  51. package/dist/components/step-list/step-list.js.map +1 -1
  52. package/dist/components/workflow-run-duration-label.js +1 -1
  53. package/dist/components/workflow-run-duration-label.js.map +1 -1
  54. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  55. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  56. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  59. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  65. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  66. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.js +8 -13
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  72. package/dist/components/workflow-run-tabs/index.d.ts +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  74. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  75. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +2 -2
  76. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +9 -2
  78. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -1
  79. package/dist/components/workflow-run-tabs/run-annotation-list.js +28 -19
  80. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -1
  81. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  82. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +5 -5
  83. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  84. package/dist/components/workflow-run-view/run-workspace-nav.js +7 -7
  85. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  86. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  87. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  88. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.js +72 -12
  90. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  91. package/dist/core/entities/job-execution.d.ts +7 -0
  92. package/dist/core/entities/job-execution.d.ts.map +1 -1
  93. package/dist/core/entities/job-execution.js.map +1 -1
  94. package/dist/core/entities/job.d.ts +9 -0
  95. package/dist/core/entities/job.d.ts.map +1 -1
  96. package/dist/core/entities/job.js.map +1 -1
  97. package/dist/core/entities/step-attempt.d.ts +29 -0
  98. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  99. package/dist/core/entities/step-attempt.js.map +1 -1
  100. package/dist/core/entities/step.d.ts +3 -1
  101. package/dist/core/entities/step.d.ts.map +1 -1
  102. package/dist/core/entities/step.js.map +1 -1
  103. package/dist/core/run-annotation.d.ts +6 -0
  104. package/dist/core/run-annotation.d.ts.map +1 -1
  105. package/dist/core/run-annotation.js.map +1 -1
  106. package/dist/core/workflow-run.d.ts +1 -1
  107. package/dist/core/workflow-run.d.ts.map +1 -1
  108. package/dist/core/workflow-run.js.map +1 -1
  109. package/dist/hooks/api/annotations.d.ts +63 -0
  110. package/dist/hooks/api/annotations.d.ts.map +1 -0
  111. package/dist/hooks/api/annotations.js +145 -0
  112. package/dist/hooks/api/annotations.js.map +1 -0
  113. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  114. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  115. package/dist/hooks/api/step-attempt-detail.js +43 -0
  116. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  117. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  118. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  119. package/dist/hooks/api/workflow-run-mapper.js +47 -0
  120. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  121. package/dist/pages/project-workflows-page.js +19 -19
  122. package/dist/pages/project-workflows-page.js.map +1 -1
  123. package/dist/pages/workflow-run-list-page.js +1 -1
  124. package/dist/pages/workflow-run-list-page.js.map +1 -1
  125. package/dist/tsconfig.test.tsbuildinfo +1 -1
  126. package/package.json +10 -10
  127. package/src/components/identifier/identifier.tsx +2 -2
  128. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  129. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  130. package/src/components/job-detail/job-context-panel.tsx +237 -0
  131. package/src/components/job-detail/job-detail-header.tsx +52 -46
  132. package/src/components/job-detail/job-detail-view.stories.tsx +555 -53
  133. package/src/components/job-detail/job-detail-view.tsx +87 -45
  134. package/src/components/job-detail/job-empty-states.tsx +23 -10
  135. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  136. package/src/components/job-detail/json-code.tsx +120 -0
  137. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  138. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  139. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  140. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  141. package/src/components/job-detail/step-troubleshooting.tsx +537 -0
  142. package/src/components/job-graph/job-graph-content.tsx +4 -3
  143. package/src/components/job-graph/job-node.tsx +3 -3
  144. package/src/components/step-list/index.ts +1 -0
  145. package/src/components/step-list/step-list-model.ts +5 -0
  146. package/src/components/step-list/step-list.test.tsx +75 -2
  147. package/src/components/step-list/step-list.tsx +73 -20
  148. package/src/components/workflow-run-duration-label.tsx +1 -1
  149. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  150. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  151. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  152. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  153. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  154. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  155. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  156. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  157. package/src/components/workflow-run-summary/workflow-run-summary.tsx +11 -11
  158. package/src/components/workflow-run-tabs/index.ts +5 -1
  159. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +2 -2
  160. package/src/components/workflow-run-tabs/run-annotation-list.tsx +25 -10
  161. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +5 -5
  162. package/src/components/workflow-run-view/run-workspace-nav.tsx +7 -7
  163. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  164. package/src/components/workflow-run-view/workflow-run-view.tsx +114 -14
  165. package/src/core/entities/job-execution.ts +7 -0
  166. package/src/core/entities/job.ts +9 -0
  167. package/src/core/entities/step-attempt.ts +33 -0
  168. package/src/core/entities/step.ts +3 -1
  169. package/src/core/run-annotation.ts +6 -0
  170. package/src/core/workflow-run.ts +4 -0
  171. package/src/hooks/api/annotations.test.ts +84 -0
  172. package/src/hooks/api/annotations.ts +240 -0
  173. package/src/hooks/api/step-attempt-detail.ts +67 -0
  174. package/src/hooks/api/workflow-run-mapper.ts +46 -0
  175. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  176. package/src/pages/project-workflows-page.tsx +19 -19
  177. package/src/pages/workflow-job-detail-page.test.tsx +52 -1
  178. package/src/pages/workflow-run-list-page.tsx +1 -1
  179. package/test/fixtures/workflow-run.ts +7 -0
  180. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,84 @@
1
+ import {
2
+ workflowRunAnnotationSummaryQueryOptions,
3
+ workflowRunAnnotationsQueryOptions,
4
+ } from './annotations.js';
5
+
6
+ const WORKFLOW_RUN_ID = '11111111-1111-4111-8111-111111111111';
7
+
8
+ describe('annotation query polling', () => {
9
+ it('polls enabled list and summary queries by default', () => {
10
+ const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1);
11
+ const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1);
12
+
13
+ expect(summary.refetchInterval).toBe(5_000);
14
+ expect(refetchIntervalForPages(list, 1)).toBe(5_000);
15
+ });
16
+
17
+ it('polls when enabled explicitly for both query builders', () => {
18
+ const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
19
+ polling: true,
20
+ });
21
+ const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
22
+ polling: true,
23
+ });
24
+
25
+ expect(summary.refetchInterval).toBe(5_000);
26
+ expect(refetchIntervalForPages(list, 1)).toBe(5_000);
27
+ });
28
+
29
+ it('does not poll disabled query builders', () => {
30
+ const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
31
+ enabled: false,
32
+ });
33
+ const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
34
+ enabled: false,
35
+ });
36
+
37
+ expect(summary.refetchInterval).toBe(false);
38
+ expect(list.refetchInterval).toBe(false);
39
+ });
40
+
41
+ it('stops summary polling when the owning run is terminal', () => {
42
+ const options = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
43
+ polling: false,
44
+ });
45
+
46
+ expect(options.refetchInterval).toBe(false);
47
+ });
48
+
49
+ it('stops annotation list polling when the owning run is terminal', () => {
50
+ const options = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
51
+ polling: false,
52
+ });
53
+
54
+ expect(options.refetchInterval).toBe(false);
55
+ });
56
+
57
+ it('stops annotation list polling after the first page has been loaded', () => {
58
+ const options = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1);
59
+
60
+ expect(refetchIntervalForPages(options, 2)).toBe(false);
61
+ });
62
+
63
+ it('keeps execution-scoped summaries on a distinct reusable query key', () => {
64
+ const runSummary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1);
65
+ const executionSummary = workflowRunAnnotationSummaryQueryOptions(
66
+ WORKFLOW_RUN_ID,
67
+ 1,
68
+ '22222222-2222-4222-8222-222222222222',
69
+ );
70
+
71
+ expect(runSummary.queryKey).not.toEqual(executionSummary.queryKey);
72
+ });
73
+ });
74
+
75
+ function refetchIntervalForPages(
76
+ options: ReturnType<typeof workflowRunAnnotationsQueryOptions>,
77
+ pageCount: number,
78
+ ) {
79
+ if (typeof options.refetchInterval !== 'function') return options.refetchInterval;
80
+
81
+ return options.refetchInterval({
82
+ state: {data: {pages: Array.from({length: pageCount}, () => ({}))}},
83
+ } as never);
84
+ }
@@ -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,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});