@shipfox/client-workflows 20.0.0 → 22.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 (118) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +51 -0
  3. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  4. package/dist/components/job-detail/job-detail-view.js +232 -44
  5. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  6. package/dist/components/job-detail/job-empty-states.js +2 -2
  7. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  8. package/dist/components/job-detail/step-attempt-log-panel.d.ts +7 -1
  9. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  10. package/dist/components/job-detail/step-attempt-log-panel.js +37 -10
  11. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  12. package/dist/components/job-graph/job-graph-content.js +2 -2
  13. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  14. package/dist/components/step-list/step-list-model.d.ts +1 -0
  15. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  16. package/dist/components/step-list/step-list-model.js +1 -1
  17. package/dist/components/step-list/step-list-model.js.map +1 -1
  18. package/dist/components/step-list/step-list.d.ts +2 -1
  19. package/dist/components/step-list/step-list.d.ts.map +1 -1
  20. package/dist/components/step-list/step-list.js +25 -9
  21. package/dist/components/step-list/step-list.js.map +1 -1
  22. package/dist/components/workflow-run-list/job-status-strip.d.ts +9 -1
  23. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -1
  24. package/dist/components/workflow-run-list/job-status-strip.js +22 -11
  25. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  26. package/dist/components/workflow-run-list/workflow-run-filters.js +1 -1
  27. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  28. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  29. package/dist/components/workflow-run-list/workflow-run-list-content.js +13 -7
  30. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  31. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  32. package/dist/components/workflow-run-list/workflow-run-list-states.js +43 -38
  33. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  34. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  35. package/dist/components/workflow-run-list/workflow-run-list-view.js +34 -22
  36. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  37. package/dist/components/workflow-run-list/workflow-run-row.d.ts +3 -3
  38. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  39. package/dist/components/workflow-run-list/workflow-run-row.js +25 -18
  40. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  41. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  42. package/dist/components/workflow-run-summary/workflow-run-summary.js +4 -4
  43. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  44. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  45. package/dist/components/workflow-run-view/run-workspace-nav.js +20 -7
  46. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  47. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  48. package/dist/components/workflow-run-view/workflow-run-states.js +1 -1
  49. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  50. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  51. package/dist/components/workflow-run-view/workflow-run-view.js +103 -78
  52. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  53. package/dist/components/workflow-source-panel/workflow-source-content.js +1 -1
  54. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -1
  55. package/dist/pages/project-workflows-page.d.ts.map +1 -1
  56. package/dist/pages/project-workflows-page.js +171 -158
  57. package/dist/pages/project-workflows-page.js.map +1 -1
  58. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -1
  59. package/dist/pages/workflow-job-detail-page.js +3 -2
  60. package/dist/pages/workflow-job-detail-page.js.map +1 -1
  61. package/dist/pages/workflow-run-detail-page.js +2 -1
  62. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  63. package/dist/pages/workflow-run-list-page.d.ts.map +1 -1
  64. package/dist/pages/workflow-run-list-page.js +9 -11
  65. package/dist/pages/workflow-run-list-page.js.map +1 -1
  66. package/dist/routes/job-detail.d.ts +1 -1
  67. package/dist/routes/job-detail.js +1 -1
  68. package/dist/routes/job-detail.js.map +1 -1
  69. package/dist/routes/run-detail.d.ts +1 -1
  70. package/dist/routes/run-detail.js +1 -1
  71. package/dist/routes/run-detail.js.map +1 -1
  72. package/dist/routes/runs.d.ts +1 -1
  73. package/dist/routes/runs.js +1 -1
  74. package/dist/routes/runs.js.map +1 -1
  75. package/dist/routes/workflows.d.ts +3 -0
  76. package/dist/routes/workflows.js +3 -0
  77. package/dist/routes/workflows.js.map +1 -1
  78. package/dist/tsconfig.test.tsbuildinfo +1 -1
  79. package/package.json +8 -8
  80. package/src/components/job-detail/job-detail-view.tsx +264 -49
  81. package/src/components/job-detail/job-empty-states.tsx +2 -2
  82. package/src/components/job-detail/step-attempt-log-panel.test.tsx +30 -12
  83. package/src/components/job-detail/step-attempt-log-panel.tsx +68 -10
  84. package/src/components/job-graph/job-graph-content.tsx +2 -2
  85. package/src/components/job-graph/job-graph.stories.tsx +1 -1
  86. package/src/components/job-graph/job-node.stories.tsx +1 -1
  87. package/src/components/step-list/step-list-model.ts +1 -1
  88. package/src/components/step-list/step-list.test.tsx +29 -1
  89. package/src/components/step-list/step-list.tsx +29 -11
  90. package/src/components/workflow-run-list/job-status-strip.tsx +25 -12
  91. package/src/components/workflow-run-list/workflow-run-filters.tsx +1 -1
  92. package/src/components/workflow-run-list/workflow-run-list-content.tsx +7 -2
  93. package/src/components/workflow-run-list/workflow-run-list-states.tsx +42 -41
  94. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +53 -4
  95. package/src/components/workflow-run-list/workflow-run-list-view.tsx +28 -22
  96. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +9 -20
  97. package/src/components/workflow-run-list/workflow-run-row.tsx +26 -19
  98. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +3 -3
  99. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +11 -2
  100. package/src/components/workflow-run-summary/workflow-run-summary.tsx +4 -7
  101. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +40 -2
  102. package/src/components/workflow-run-view/run-workspace-nav.tsx +28 -9
  103. package/src/components/workflow-run-view/workflow-run-states.tsx +1 -4
  104. package/src/components/workflow-run-view/workflow-run-view.stories.tsx +142 -0
  105. package/src/components/workflow-run-view/workflow-run-view.test.tsx +42 -8
  106. package/src/components/workflow-run-view/workflow-run-view.tsx +112 -96
  107. package/src/components/workflow-source-panel/workflow-source-content.tsx +2 -2
  108. package/src/pages/project-workflows-page.tsx +135 -125
  109. package/src/pages/workflow-job-detail-page.test.tsx +32 -0
  110. package/src/pages/workflow-job-detail-page.tsx +5 -1
  111. package/src/pages/workflow-run-detail-page.tsx +1 -1
  112. package/src/pages/workflow-run-list-page.tsx +9 -11
  113. package/src/pages/workflow-run-pages.test.tsx +26 -0
  114. package/src/routes/job-detail.tsx +1 -1
  115. package/src/routes/run-detail.tsx +1 -1
  116. package/src/routes/runs.tsx +1 -1
  117. package/src/routes/workflows.tsx +1 -0
  118. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,142 @@
1
+ import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
2
+ import {configureApiClient} from '@shipfox/client-api';
3
+ import type {Decorator, Meta, StoryObj} from '@storybook/react';
4
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
5
+ import {type ReactNode, useEffect, useState} from 'react';
6
+ import {
7
+ runAttemptsResponseDto,
8
+ workflowJobDto,
9
+ workflowRunDetailDto,
10
+ } from '#test/fixtures/workflow-run.js';
11
+ import {WorkflowRunView} from './workflow-run-view.js';
12
+
13
+ const PROJECT_ID = '22222222-2222-4222-8222-222222222222';
14
+ const RUN_ID = '11111111-1111-4111-8111-111111111111';
15
+ const RUN_STARTED_AT = '2026-06-26T11:57:00.000Z';
16
+ const RUN_FINISHED_AT = '2026-06-26T11:59:00.000Z';
17
+
18
+ const RUN_RESPONSE: WorkflowRunDetailResponseDto = workflowRunDetailDto({
19
+ id: RUN_ID,
20
+ project_id: PROJECT_ID,
21
+ name: 'deploy-web',
22
+ status: 'succeeded',
23
+ started_at: RUN_STARTED_AT,
24
+ finished_at: RUN_FINISHED_AT,
25
+ has_started_job_execution: true,
26
+ jobs: [
27
+ workflowJobDto({
28
+ key: 'build',
29
+ name: 'build',
30
+ status: 'succeeded',
31
+ position: 0,
32
+ }),
33
+ workflowJobDto({
34
+ key: 'deploy',
35
+ name: 'deploy',
36
+ status: 'succeeded',
37
+ position: 1,
38
+ dependencies: ['build'],
39
+ }),
40
+ workflowJobDto({
41
+ key: 'notify',
42
+ name: 'notify',
43
+ status: 'succeeded',
44
+ position: 2,
45
+ dependencies: ['deploy'],
46
+ }),
47
+ ],
48
+ });
49
+ const RUN_ATTEMPTS_RESPONSE = runAttemptsResponseDto({attempts: []});
50
+
51
+ const withRunApi: Decorator = (Story) => (
52
+ <RunWorkspaceStoryProviders>
53
+ <Story />
54
+ </RunWorkspaceStoryProviders>
55
+ );
56
+
57
+ const meta = {
58
+ title: 'Workflows/WorkflowRunView',
59
+ component: WorkflowRunView,
60
+ parameters: {
61
+ layout: 'fullscreen',
62
+ viewport: {
63
+ defaultViewport: 'wide',
64
+ options: {
65
+ wide: {
66
+ name: 'Wide',
67
+ styles: {width: '1440px', height: '720px'},
68
+ type: 'desktop',
69
+ },
70
+ },
71
+ },
72
+ globals: {
73
+ viewport: {value: 'wide'},
74
+ },
75
+ argos: {
76
+ modes: {dark: {theme: 'dark'}},
77
+ fitToContent: false,
78
+ },
79
+ },
80
+ decorators: [withRunApi],
81
+ args: {
82
+ projectId: PROJECT_ID,
83
+ workspaceSlug: 'acme',
84
+ projectSlug: 'platform',
85
+ workflowRunId: RUN_ID,
86
+ },
87
+ } satisfies Meta<typeof WorkflowRunView>;
88
+
89
+ export default meta;
90
+ type Story = StoryObj<typeof meta>;
91
+
92
+ export const WideViewport: Story = {};
93
+
94
+ function RunWorkspaceStoryProviders({children}: {children: ReactNode}) {
95
+ const [queryClient] = useState(
96
+ () => new QueryClient({defaultOptions: {queries: {retry: false}}}),
97
+ );
98
+ const [configured, setConfigured] = useState(false);
99
+
100
+ useEffect(() => {
101
+ configureApiClient({
102
+ baseUrl: 'https://api.example.test',
103
+ fetchImpl: (input) => {
104
+ const url =
105
+ input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
106
+ const path = new URL(url, 'https://api.example.test').pathname;
107
+ const response =
108
+ path === '/annotations'
109
+ ? {
110
+ body: {annotations: [], has_more: false, next_cursor: null},
111
+ status: 200,
112
+ }
113
+ : path === `/workflows/runs/${RUN_ID}/attempts`
114
+ ? {body: RUN_ATTEMPTS_RESPONSE, status: 200}
115
+ : path === `/workflows/runs/${RUN_ID}`
116
+ ? {body: RUN_RESPONSE, status: 200}
117
+ : {body: {code: 'not-found'}, status: 404};
118
+ return new Response(JSON.stringify(response.body), {
119
+ status: response.status,
120
+ headers: {'content-type': 'application/json'},
121
+ });
122
+ },
123
+ });
124
+ setConfigured(true);
125
+
126
+ return () => {
127
+ configureApiClient({baseUrl: '', fetchImpl: undefined});
128
+ };
129
+ }, []);
130
+
131
+ if (!configured) return null;
132
+
133
+ return (
134
+ <QueryClientProvider client={queryClient}>
135
+ <div className="flex h-[720px] min-w-[1440px] w-[1440px] bg-background-subtle-base">
136
+ <div className="flex min-h-0 min-w-0 w-full flex-1 flex-col px-frame py-frame">
137
+ {children}
138
+ </div>
139
+ </div>
140
+ </QueryClientProvider>
141
+ );
142
+ }
@@ -49,23 +49,57 @@ describe('WorkflowRunView', () => {
49
49
  const summary = await screen.findByRole('region', {name: 'deploy-web'});
50
50
  expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
51
51
  expect(screen.getByRole('navigation', {name: 'Run workspace'})).toBeInTheDocument();
52
- expect(
53
- screen
54
- .getByRole('navigation', {name: 'Run workspace'})
55
- .closest('[data-run-workspace-layout]'),
56
- ).toHaveClass('border-t', 'border-border-neutral-base', 'min-[768px]:flex-row');
52
+ const workspaceLayout = screen
53
+ .getByRole('navigation', {name: 'Run workspace'})
54
+ .closest('[data-run-workspace-layout]');
55
+ expect(workspaceLayout).toHaveClass('border-t', 'border-border-neutral-base');
56
+ expect(workspaceLayout).not.toHaveClass('max-w-[1360px]');
57
+ const workspaceFrame = workspaceLayout?.querySelector('[data-run-workspace-frame]');
58
+ expect(workspaceFrame).toHaveClass('w-full', 'min-[768px]:flex-row');
59
+ expect(workspaceFrame).not.toHaveClass('mx-auto', 'max-w-[calc(240px_+_1120px)]');
57
60
  expect(screen.getByRole('link', {name: 'Summary'})).toHaveAttribute('aria-current', 'page');
58
61
  expect(screen.getByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
59
62
  expect(screen.getByRole('heading', {name: 'Run details'})).toBeInTheDocument();
60
- expect(screen.getByRole('region', {name: 'All jobs summary'})).toBeInTheDocument();
63
+ const allJobsSummary = screen.getByRole('region', {name: 'All jobs summary'});
64
+ expect(allJobsSummary).toBeInTheDocument();
65
+ expect(allJobsSummary.querySelector('[data-slot="panel"]')).not.toBeNull();
66
+ expect(allJobsSummary.querySelector('[data-slot="panel-body"]')).toHaveClass(
67
+ 'bg-background-components-base',
68
+ );
61
69
  expect(screen.getByRole('region', {name: 'Workflow jobs'})).toBeInTheDocument();
62
- expect(container.querySelector('[data-run-workspace-content]')).toHaveClass(
70
+ expect(container.querySelector('[data-run-workspace-content]')).toHaveClass('flex-1');
71
+ expect(container.querySelector('[data-run-workspace-content]')).not.toHaveClass(
63
72
  'bg-background-neutral-base',
64
- 'flex-1',
65
73
  );
66
74
  expect(screen.queryByRole('tab', {name: 'Jobs'})).not.toBeInTheDocument();
67
75
  });
68
76
 
77
+ test.each([
78
+ {tab: undefined, region: 'All jobs summary'},
79
+ {tab: 'source', region: 'Workflow source'},
80
+ {tab: 'annotations', region: 'Run annotations'},
81
+ ] as const)('does not cap the $region section at 1120px', async ({tab, region}) => {
82
+ configureRunFetch();
83
+
84
+ renderView({tab});
85
+
86
+ const section = await screen.findByRole('region', {name: region});
87
+ expect(section.firstElementChild).not.toHaveClass('mx-auto', 'px-frame', 'max-w-[1120px]');
88
+ expect(section.querySelector('[data-slot="panel"]')).not.toBeNull();
89
+ });
90
+
91
+ test('keeps dedicated job content on the full-width data surface', async () => {
92
+ configureRunFetch();
93
+
94
+ renderView({jobContent: <div>Job logs</div>});
95
+
96
+ const jobContent = await screen.findByText('Job logs');
97
+ const workspaceFrame = jobContent.closest('[data-run-workspace-frame]');
98
+
99
+ expect(workspaceFrame).toHaveClass('w-full', 'min-[768px]:flex-row');
100
+ expect(workspaceFrame).not.toHaveClass('max-w-[calc(240px_+_1120px)]');
101
+ });
102
+
69
103
  test('treats the removed Jobs tab URL as the graph Summary', async () => {
70
104
  configureRunFetch();
71
105
 
@@ -1,6 +1,7 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
2
  import {QueryLoadError} from '@shipfox/client-ui';
3
3
  import {EmptyState} from '@shipfox/react-ui/empty-state';
4
+ import {Panel, PanelBody, PanelHeader} from '@shipfox/react-ui/panel';
4
5
  import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
5
6
  import {
6
7
  Select,
@@ -309,47 +310,49 @@ function RunViewContent({
309
310
 
310
311
  <div
311
312
  data-run-workspace-layout
312
- className="flex min-h-0 min-w-0 flex-1 flex-col border-t border-border-neutral-base min-[768px]:flex-row"
313
+ className="flex min-h-0 min-w-0 flex-1 flex-col border-t border-border-neutral-base"
313
314
  >
314
- {runData && workspaceSlug && projectSlug ? (
315
- <RunWorkspaceNav
316
- workspaceSlug={workspaceSlug}
317
- projectSlug={projectSlug}
318
- run={runData}
319
- activeSection={activeSection}
320
- currentJobId={activeJobId}
321
- jobSearch={jobSearch}
322
- annotationSummary={annotationSummary}
323
- />
324
- ) : (
325
- <RunWorkspaceNavSkeleton />
326
- )}
327
-
328
315
  <div
329
- data-run-workspace-content
330
- className="flex min-h-0 min-w-0 flex-1 flex-col bg-background-neutral-base"
316
+ data-run-workspace-frame
317
+ className="flex min-h-0 min-w-0 w-full flex-1 flex-col min-[768px]:flex-row"
331
318
  >
332
- {runData && jobContent ? (
333
- jobContent
334
- ) : runData ? (
335
- <RunSectionContent
336
- section={activeSection}
337
- run={runData}
338
- annotations={annotations}
339
- annotationSummary={annotationSummary}
319
+ {runData && workspaceSlug && projectSlug ? (
320
+ <RunWorkspaceNav
340
321
  workspaceSlug={workspaceSlug}
341
322
  projectSlug={projectSlug}
342
- selection={selection}
343
- selectedJobId={selection?.jobId}
344
- onSelectGraphJob={selectGraphJob}
345
- onSelectAnnotationJob={selectAnnotationJob}
346
- onClearAnnotationFilters={clearAnnotationFilters}
347
- sourceSnapshot={sourceSnapshot}
348
- highlightedLineRange={highlightedLineRange}
323
+ run={runData}
324
+ activeSection={activeSection}
325
+ currentJobId={activeJobId}
326
+ jobSearch={jobSearch}
327
+ annotationSummary={annotationSummary}
349
328
  />
350
329
  ) : (
351
- <div className="min-h-0 flex-1 overflow-auto p-panel">{loadingOrError}</div>
330
+ <RunWorkspaceNavSkeleton />
352
331
  )}
332
+
333
+ <div data-run-workspace-content className="flex min-h-0 min-w-0 flex-1 flex-col">
334
+ {runData && jobContent ? (
335
+ jobContent
336
+ ) : runData ? (
337
+ <RunSectionContent
338
+ section={activeSection}
339
+ run={runData}
340
+ annotations={annotations}
341
+ annotationSummary={annotationSummary}
342
+ workspaceSlug={workspaceSlug}
343
+ projectSlug={projectSlug}
344
+ selection={selection}
345
+ selectedJobId={selection?.jobId}
346
+ onSelectGraphJob={selectGraphJob}
347
+ onSelectAnnotationJob={selectAnnotationJob}
348
+ onClearAnnotationFilters={clearAnnotationFilters}
349
+ sourceSnapshot={sourceSnapshot}
350
+ highlightedLineRange={highlightedLineRange}
351
+ />
352
+ ) : (
353
+ <div className="min-h-0 flex-1 overflow-auto p-panel">{loadingOrError}</div>
354
+ )}
355
+ </div>
353
356
  </div>
354
357
  </div>
355
358
  </div>
@@ -395,16 +398,20 @@ function RunSectionContent({
395
398
  aria-label="All jobs summary"
396
399
  className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
397
400
  >
398
- <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-group px-frame">
401
+ <div className="flex w-full flex-col gap-group">
399
402
  <Text as="h2" className="sr-only">
400
403
  All jobs summary
401
404
  </Text>
402
- <JobGraph
403
- run={run}
404
- selectedJobId={selectedJobId}
405
- onSelectedJobChange={onSelectGraphJob}
406
- className="min-h-160 overflow-hidden"
407
- />
405
+ <Panel className="min-h-160">
406
+ <PanelBody className="min-h-160 bg-background-components-base p-0">
407
+ <JobGraph
408
+ run={run}
409
+ selectedJobId={selectedJobId}
410
+ onSelectedJobChange={onSelectGraphJob}
411
+ className="min-h-160 overflow-hidden"
412
+ />
413
+ </PanelBody>
414
+ </Panel>
408
415
  </div>
409
416
  </section>
410
417
  );
@@ -431,28 +438,33 @@ function RunSectionContent({
431
438
  aria-label="Workflow source"
432
439
  className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
433
440
  >
434
- <div className="mx-auto flex min-h-full w-full max-w-[1120px] flex-col px-frame">
441
+ <div className="flex min-h-full w-full flex-col">
435
442
  <Text as="h2" className="sr-only">
436
443
  Workflow source
437
444
  </Text>
438
- {sourceSnapshot ? (
439
- <WorkflowSourceContent
440
- source={sourceSnapshot}
441
- highlightedLineRange={highlightedLineRange}
442
- scrollHighlightedIntoView
443
- />
444
- ) : (
445
- <EmptyState
446
- className="min-h-160 w-full"
447
- icon="fileDamageLine"
448
- title="Source snapshot unavailable"
449
- description={
450
- run.isTemporary
451
- ? 'Temporary runs do not capture workflow source.'
452
- : 'This run was created before workflow source snapshots were captured.'
453
- }
454
- />
455
- )}
445
+ <Panel className="min-h-160 flex-1">
446
+ <PanelBody className="min-h-160 flex-1 p-0">
447
+ {sourceSnapshot ? (
448
+ <WorkflowSourceContent
449
+ source={sourceSnapshot}
450
+ highlightedLineRange={highlightedLineRange}
451
+ scrollHighlightedIntoView
452
+ />
453
+ ) : (
454
+ <EmptyState
455
+ variant="panel"
456
+ className="min-h-160"
457
+ icon="fileDamageLine"
458
+ title="Source snapshot unavailable"
459
+ description={
460
+ run.isTemporary
461
+ ? 'Temporary runs do not capture workflow source.'
462
+ : 'This run was created before workflow source snapshots were captured.'
463
+ }
464
+ />
465
+ )}
466
+ </PanelBody>
467
+ </Panel>
456
468
  </div>
457
469
  </section>
458
470
  );
@@ -526,45 +538,49 @@ function RunAnnotationsSection({
526
538
  aria-label="Run annotations"
527
539
  className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
528
540
  >
529
- <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-group px-frame">
541
+ <div className="flex w-full flex-col">
530
542
  <Text as="h2" className="sr-only">
531
543
  Annotations
532
544
  </Text>
533
- <div className="flex flex-wrap items-center justify-between gap-inline">
534
- <RunAnnotationSummaryLine
535
- summary={annotationSummary}
536
- workspaceSlug={workspaceSlug}
537
- projectSlug={projectSlug}
538
- workflowRunId={run.id}
539
- search={selection}
540
- />
541
- <AnnotationJobFilter
542
- jobs={run.jobs}
543
- selectedJobId={selectedJob?.id}
544
- onSelect={onSelectAnnotationJob}
545
- />
546
- </div>
547
- <RunAnnotationList
548
- // Remounting on a filter or run-attempt change resets the render window, so the next
549
- // list never inherits a "show more" position from different data.
550
- key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
551
- query={annotations.query}
552
- entries={entries}
553
- derivedAnnotations={derivedAnnotations}
554
- workspaceSlug={workspaceSlug}
555
- projectSlug={projectSlug}
556
- workflowRunId={run.id}
557
- runAttempt={run.runAttempt.attempt}
558
- // A run with no annotations at all offers no filter to clear, whatever the URL says.
559
- filtered={Boolean(
560
- (severity || selectedJob || selection?.annotation) &&
561
- ((annotationSummary?.total ?? 0) > 0 || hasSynthesizedJobAnnotations),
562
- )}
563
- filteredJobName={selectedJob?.displayName}
564
- filteredSeverity={severity}
565
- onClearFilters={onClearAnnotationFilters}
566
- selectedAnnotationId={selection?.annotation}
567
- />
545
+ <Panel>
546
+ <PanelHeader className="flex-wrap">
547
+ <RunAnnotationSummaryLine
548
+ summary={annotationSummary}
549
+ workspaceSlug={workspaceSlug}
550
+ projectSlug={projectSlug}
551
+ workflowRunId={run.id}
552
+ search={selection}
553
+ />
554
+ <AnnotationJobFilter
555
+ jobs={run.jobs}
556
+ selectedJobId={selectedJob?.id}
557
+ onSelect={onSelectAnnotationJob}
558
+ />
559
+ </PanelHeader>
560
+ <PanelBody className="gap-group p-panel">
561
+ <RunAnnotationList
562
+ // Remounting on a filter or run-attempt change resets the render window, so the next
563
+ // list never inherits a "show more" position from different data.
564
+ key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
565
+ query={annotations.query}
566
+ entries={entries}
567
+ derivedAnnotations={derivedAnnotations}
568
+ workspaceSlug={workspaceSlug}
569
+ projectSlug={projectSlug}
570
+ workflowRunId={run.id}
571
+ runAttempt={run.runAttempt.attempt}
572
+ // A run with no annotations at all offers no filter to clear, whatever the URL says.
573
+ filtered={Boolean(
574
+ (severity || selectedJob || selection?.annotation) &&
575
+ ((annotationSummary?.total ?? 0) > 0 || hasSynthesizedJobAnnotations),
576
+ )}
577
+ filteredJobName={selectedJob?.displayName}
578
+ filteredSeverity={severity}
579
+ onClearFilters={onClearAnnotationFilters}
580
+ selectedAnnotationId={selection?.annotation}
581
+ />
582
+ </PanelBody>
583
+ </Panel>
568
584
  </div>
569
585
  </section>
570
586
  );
@@ -666,7 +682,7 @@ function RunWorkspaceNavSkeleton() {
666
682
  return (
667
683
  <aside
668
684
  aria-label="Loading run navigation"
669
- className="hidden w-240 shrink-0 border-r border-border-neutral-base bg-background-neutral-background p-panel-compact min-[768px]:block"
685
+ className="hidden w-240 shrink-0 p-panel-compact min-[768px]:block"
670
686
  >
671
687
  <div className="flex flex-col gap-group">
672
688
  <div className="h-32 rounded-4 bg-background-components-subtle" />
@@ -63,8 +63,8 @@ export function WorkflowSourceContent({
63
63
  value={item.filename}
64
64
  className={cn(
65
65
  'min-h-full px-0 pb-0',
66
- '[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base [&>div]:dark:bg-background-contrast-base',
67
- '[&_code]:!text-sm [&_code]:!text-foreground-neutral-on-inverted [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-neutral-muted',
66
+ '[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base',
67
+ '[&_code]:!text-sm [&_code]:!text-foreground-contrast-primary [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-contrast-secondary',
68
68
  )}
69
69
  >
70
70
  <CodeBlockContent