@shipfox/client-workflows 16.0.0 → 18.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 (182) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/dist/components/identifier/identifier.js +2 -2
  4. package/dist/components/identifier/identifier.js.map +1 -1
  5. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  6. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  8. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  9. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  10. package/dist/components/job-detail/job-context-panel.js +228 -0
  11. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  12. package/dist/components/job-detail/job-detail-header.d.ts +3 -1
  13. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  14. package/dist/components/job-detail/job-detail-header.js +54 -48
  15. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  16. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  17. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.js +100 -57
  19. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  20. package/dist/components/job-detail/job-empty-states.d.ts +4 -0
  21. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.js +60 -11
  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 +497 -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 +6 -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.d.ts.map +1 -1
  85. package/dist/components/workflow-run-view/run-workspace-nav.js +11 -10
  86. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  87. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  88. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  90. package/dist/components/workflow-run-view/workflow-run-view.js +72 -12
  91. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  92. package/dist/core/entities/job-execution.d.ts +9 -0
  93. package/dist/core/entities/job-execution.d.ts.map +1 -1
  94. package/dist/core/entities/job-execution.js.map +1 -1
  95. package/dist/core/entities/job.d.ts +10 -1
  96. package/dist/core/entities/job.d.ts.map +1 -1
  97. package/dist/core/entities/job.js.map +1 -1
  98. package/dist/core/entities/step-attempt.d.ts +29 -0
  99. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  100. package/dist/core/entities/step-attempt.js.map +1 -1
  101. package/dist/core/entities/step.d.ts +3 -1
  102. package/dist/core/entities/step.d.ts.map +1 -1
  103. package/dist/core/entities/step.js.map +1 -1
  104. package/dist/core/run-annotation.d.ts +6 -0
  105. package/dist/core/run-annotation.d.ts.map +1 -1
  106. package/dist/core/run-annotation.js.map +1 -1
  107. package/dist/core/workflow-run.d.ts +1 -1
  108. package/dist/core/workflow-run.d.ts.map +1 -1
  109. package/dist/core/workflow-run.js.map +1 -1
  110. package/dist/hooks/api/annotations.d.ts +63 -0
  111. package/dist/hooks/api/annotations.d.ts.map +1 -0
  112. package/dist/hooks/api/annotations.js +145 -0
  113. package/dist/hooks/api/annotations.js.map +1 -0
  114. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  115. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  116. package/dist/hooks/api/step-attempt-detail.js +43 -0
  117. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  118. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  119. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  120. package/dist/hooks/api/workflow-run-mapper.js +48 -0
  121. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  122. package/dist/pages/project-workflows-page.js +19 -19
  123. package/dist/pages/project-workflows-page.js.map +1 -1
  124. package/dist/pages/workflow-run-list-page.js +1 -1
  125. package/dist/pages/workflow-run-list-page.js.map +1 -1
  126. package/dist/tsconfig.test.tsbuildinfo +1 -1
  127. package/package.json +10 -10
  128. package/src/components/identifier/identifier.tsx +2 -2
  129. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  130. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  131. package/src/components/job-detail/job-context-panel.tsx +241 -0
  132. package/src/components/job-detail/job-detail-header.tsx +52 -46
  133. package/src/components/job-detail/job-detail-view.stories.tsx +555 -53
  134. package/src/components/job-detail/job-detail-view.tsx +105 -59
  135. package/src/components/job-detail/job-empty-states.test.ts +105 -1
  136. package/src/components/job-detail/job-empty-states.tsx +77 -10
  137. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  138. package/src/components/job-detail/json-code.tsx +120 -0
  139. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  140. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  141. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  142. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  143. package/src/components/job-detail/step-troubleshooting.tsx +541 -0
  144. package/src/components/job-graph/job-graph-content.tsx +4 -3
  145. package/src/components/job-graph/job-node.tsx +3 -3
  146. package/src/components/step-list/index.ts +1 -0
  147. package/src/components/step-list/step-list-model.ts +6 -0
  148. package/src/components/step-list/step-list.test.tsx +75 -2
  149. package/src/components/step-list/step-list.tsx +73 -20
  150. package/src/components/workflow-run-duration-label.tsx +1 -1
  151. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  152. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  153. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  154. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  155. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  156. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  157. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  158. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  159. package/src/components/workflow-run-summary/workflow-run-summary.tsx +11 -11
  160. package/src/components/workflow-run-tabs/index.ts +5 -1
  161. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +2 -2
  162. package/src/components/workflow-run-tabs/run-annotation-list.tsx +25 -10
  163. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +5 -5
  164. package/src/components/workflow-run-view/run-workspace-nav.tsx +18 -11
  165. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  166. package/src/components/workflow-run-view/workflow-run-view.tsx +114 -14
  167. package/src/core/entities/job-execution.ts +9 -0
  168. package/src/core/entities/job.ts +12 -1
  169. package/src/core/entities/step-attempt.ts +33 -0
  170. package/src/core/entities/step.ts +3 -1
  171. package/src/core/run-annotation.ts +6 -0
  172. package/src/core/workflow-run.ts +4 -0
  173. package/src/hooks/api/annotations.test.ts +84 -0
  174. package/src/hooks/api/annotations.ts +240 -0
  175. package/src/hooks/api/step-attempt-detail.ts +67 -0
  176. package/src/hooks/api/workflow-run-mapper.ts +47 -0
  177. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  178. package/src/pages/project-workflows-page.tsx +19 -19
  179. package/src/pages/workflow-job-detail-page.test.tsx +52 -1
  180. package/src/pages/workflow-run-list-page.tsx +1 -1
  181. package/test/fixtures/workflow-run.ts +7 -0
  182. package/tsconfig.build.tsbuildinfo +1 -1
@@ -44,6 +44,12 @@ export interface RunAnnotationSummary {
44
44
  success: number;
45
45
  /** True when the read hit its page budget, so every count is a lower bound. */
46
46
  truncated: boolean;
47
+ /** Optional per-step counts used by the step inspector without loading annotation bodies. */
48
+ stepCounts?: readonly {
49
+ stepId: string;
50
+ attempt: number;
51
+ total: number;
52
+ }[];
47
53
  }
48
54
 
49
55
  /** Everything the annotations list needs to route back to the step that emitted an annotation. */
@@ -46,6 +46,10 @@ export {
46
46
  STEP_ERROR_REASONS,
47
47
  } from './entities/step.js';
48
48
  export type {
49
+ EvaluationTraceEntry,
50
+ EvaluationTraceLimitEntry,
51
+ EvaluationTraceValueEntry,
52
+ StepAttemptDetail,
49
53
  StepAttemptDisplayDuration,
50
54
  StepGateResult,
51
55
  } from './entities/step-attempt.js';
@@ -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,15 @@ export function toJobExecution(dto: WorkflowRunJobExecutionDetailDto): JobExecut
158
165
  name: dto.name,
159
166
  status: dto.status,
160
167
  statusReason: dto.status_reason,
168
+ statusReasonMessage: dto.status_reason_message ?? null,
169
+ runner: dto.runner ?? null,
170
+ outputs: dto.outputs ?? null,
161
171
  triggerEvents: (dto.trigger_events ?? []).map(toWorkflowExecutionEvent),
162
172
  queuedAt: dto.queued_at ?? null,
163
173
  startedAt: dto.started_at ?? null,
164
174
  finishedAt: dto.finished_at ?? null,
165
175
  timedOutAt: dto.timed_out_at ?? null,
176
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
166
177
  createdAt: dto.created_at,
167
178
  updatedAt: dto.updated_at,
168
179
  steps: dto.steps.map(toStep),
@@ -179,8 +190,10 @@ export function toStep(dto: WorkflowRunStepDetailDto): Step {
179
190
  ? {startLine: dto.source_location.start_line, endLine: dto.source_location.end_line}
180
191
  : null,
181
192
  status: dto.status,
193
+ statusReason: dto.status_reason,
182
194
  type: dto.type,
183
195
  config: dto.config,
196
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
184
197
  agentConfig: toAgentStepConfig(dto),
185
198
  error: dto.error
186
199
  ? {
@@ -210,6 +223,8 @@ export function toStepAttempt(dto: StepAttemptDto, jobExecutionId: string): Step
210
223
  status: dto.status,
211
224
  exitCode: dto.exit_code ?? null,
212
225
  output: dto.output ?? null,
226
+ outputs: dto.outputs ?? dto.output ?? null,
227
+ response: dto.response ?? null,
213
228
  error: dto.error ?? null,
214
229
  gateResult: toStepGateResult(dto.gate_result),
215
230
  restartFeedback: dto.restart_feedback ?? null,
@@ -218,6 +233,16 @@ export function toStepAttempt(dto: StepAttemptDto, jobExecutionId: string): Step
218
233
  });
219
234
  }
220
235
 
236
+ export function toStepAttemptDetail(dto: StepAttemptDetailResponseDto) {
237
+ return {
238
+ stepId: dto.step_id,
239
+ attempt: dto.attempt,
240
+ authoredConfig: dto.authored_config,
241
+ config: dto.config,
242
+ evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
243
+ };
244
+ }
245
+
221
246
  function toJobListening(dto: JobListeningDto): JobListening {
222
247
  return {
223
248
  on: dto.on,
@@ -259,6 +284,28 @@ function toStepGateResult(dto: StepGateResultDto): StepGateResult {
259
284
  return dto;
260
285
  }
261
286
 
287
+ function toEvaluationTrace(trace: EvaluationTraceDto | null): EvaluationTraceEntry[] | null {
288
+ return (
289
+ trace?.map((entry) =>
290
+ 'dropped' in entry
291
+ ? entry
292
+ : {
293
+ expression: entry.expression,
294
+ roots: entry.roots,
295
+ fillTarget: entry.fill_target,
296
+ evaluatedAt: entry.evaluated_at,
297
+ field: entry.field,
298
+ ...(entry.value === undefined ? {} : {value: entry.value}),
299
+ ...(entry.truncated === undefined ? {} : {truncated: entry.truncated}),
300
+ ...(entry.expr_truncated === undefined ? {} : {exprTruncated: entry.expr_truncated}),
301
+ ...(entry.reference === undefined ? {} : {reference: entry.reference}),
302
+ ...(entry.degraded === undefined ? {} : {degraded: entry.degraded}),
303
+ ...(entry.env_key === undefined ? {} : {envKey: entry.env_key}),
304
+ },
305
+ ) ?? null
306
+ );
307
+ }
308
+
262
309
  function toAgentStepConfig(dto: WorkflowRunStepDetailDto): Step['agentConfig'] {
263
310
  if (dto.type !== 'agent') return null;
264
311
  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});