@shipfox/client-agent 21.0.0 → 22.0.1

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 (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/dist/components/add-custom-provider-card.d.ts.map +1 -1
  4. package/dist/components/add-custom-provider-card.js +11 -31
  5. package/dist/components/add-custom-provider-card.js.map +1 -1
  6. package/dist/components/available-provider-card.d.ts.map +1 -1
  7. package/dist/components/available-provider-card.js +11 -31
  8. package/dist/components/available-provider-card.js.map +1 -1
  9. package/dist/components/available-providers-grid.d.ts +0 -1
  10. package/dist/components/available-providers-grid.d.ts.map +1 -1
  11. package/dist/components/available-providers-grid.js +33 -27
  12. package/dist/components/available-providers-grid.js.map +1 -1
  13. package/dist/components/harnesses-section.d.ts.map +1 -1
  14. package/dist/components/harnesses-section.js +152 -146
  15. package/dist/components/harnesses-section.js.map +1 -1
  16. package/dist/components/model-provider-grid-skeleton.d.ts +4 -0
  17. package/dist/components/model-provider-grid-skeleton.d.ts.map +1 -0
  18. package/dist/components/model-provider-grid-skeleton.js +34 -0
  19. package/dist/components/model-provider-grid-skeleton.js.map +1 -0
  20. package/dist/components/model-providers-section.d.ts.map +1 -1
  21. package/dist/components/model-providers-section.js +268 -281
  22. package/dist/components/model-providers-section.js.map +1 -1
  23. package/dist/pages/model-provider-onboarding-page.d.ts.map +1 -1
  24. package/dist/pages/model-provider-onboarding-page.js +32 -64
  25. package/dist/pages/model-provider-onboarding-page.js.map +1 -1
  26. package/dist/routes/agents-settings.js +15 -5
  27. package/dist/routes/agents-settings.js.map +1 -1
  28. package/dist/tsconfig.test.tsbuildinfo +1 -1
  29. package/package.json +4 -4
  30. package/src/components/add-custom-provider-card.tsx +9 -23
  31. package/src/components/available-provider-card.tsx +9 -23
  32. package/src/components/available-providers-grid.tsx +36 -29
  33. package/src/components/harnesses-section.tsx +116 -109
  34. package/src/components/model-provider-grid-skeleton.tsx +21 -0
  35. package/src/components/model-providers-section.test.tsx +2 -2
  36. package/src/components/model-providers-section.tsx +208 -200
  37. package/src/pages/model-provider-onboarding-page.tsx +26 -54
  38. package/src/routes/agents-settings.tsx +7 -3
  39. package/tsconfig.build.tsbuildinfo +1 -1
@@ -17,11 +17,11 @@ import {
17
17
  ModalHeader,
18
18
  ModalTitle,
19
19
  } from '@shipfox/react-ui/modal';
20
+ import {Panel, PanelBody, PanelRow} from '@shipfox/react-ui/panel';
20
21
  import {Skeleton} from '@shipfox/react-ui/skeleton';
21
22
  import {toast} from '@shipfox/react-ui/toast';
22
23
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
23
24
  import {Header, Text} from '@shipfox/react-ui/typography';
24
- import {cn} from '@shipfox/react-ui/utils';
25
25
  import {useEffect, useMemo, useReducer, useRef, useState} from 'react';
26
26
  import {
27
27
  type ManagementModal,
@@ -46,10 +46,11 @@ import {
46
46
  useSetDefaultModelProviderMutation,
47
47
  } from '#hooks/api/model-providers.js';
48
48
  import {AddCustomProviderCard} from './add-custom-provider-card.js';
49
- import {AvailableProvidersGrid, PROVIDER_GRID_CLASS} from './available-providers-grid.js';
49
+ import {AvailableProvidersGrid} from './available-providers-grid.js';
50
50
  import {ChangeDefaultModelForm} from './change-default-model-form.js';
51
51
  import {CustomModelProviderForm} from './custom-model-provider-form.js';
52
52
  import {modelProviderConfigErrorToFormError} from './form-errors.js';
53
+ import {ModelProviderGridSkeleton} from './model-provider-grid-skeleton.js';
53
54
  import {ModelProviderUsageModal} from './model-provider-usage-modal.js';
54
55
  import {
55
56
  type ModelProviderUsageTarget,
@@ -58,9 +59,6 @@ import {
58
59
  } from './model-provider-usage-target.js';
59
60
  import {ModelProviderTestAndSaveForm} from './test-and-save-form.js';
60
61
 
61
- const SURFACE_CLASS =
62
- 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
63
-
64
62
  type UsageTarget = {
65
63
  target: ModelProviderUsageTarget;
66
64
  initialModel: string | null;
@@ -122,9 +120,6 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
122
120
  >
123
121
  <div className="flex flex-col gap-tight">
124
122
  <Header variant="h3">Configured providers</Header>
125
- <Text size="sm" className="text-foreground-neutral-muted">
126
- Workspace credentials available to agent steps.
127
- </Text>
128
123
  </div>
129
124
 
130
125
  {configsQuery.isPending ? (
@@ -132,91 +127,99 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
132
127
  ) : null}
133
128
 
134
129
  {configsQuery.isError && configsQuery.data === undefined ? (
135
- <div className={cn(SURFACE_CLASS, 'px-row')}>
136
- <QueryLoadError query={configsQuery} subject="model provider configs" />
137
- </div>
130
+ <Panel>
131
+ <QueryLoadError query={configsQuery} subject="model provider configs" variant="panel" />
132
+ </Panel>
138
133
  ) : null}
139
134
 
140
135
  {configsQuery.data !== undefined && configs.length === 0 ? (
141
- <div className={cn(SURFACE_CLASS, 'px-row')}>
136
+ <Panel>
142
137
  <EmptyState
143
138
  icon="key2Line"
144
139
  title="No providers configured"
145
140
  description="Configure a provider below to run agent steps with workspace-managed credentials."
141
+ variant="panel"
146
142
  />
147
- </div>
143
+ </Panel>
148
144
  ) : null}
149
145
 
150
146
  {configs.length > 0 ? (
151
- <ul className={cn('divide-y divide-border-neutral-base', SURFACE_CLASS)}>
152
- {configs.map((config) => {
153
- const catalogEntry = providerById.get(config.providerId);
154
- const entry =
155
- catalogEntry && isSupportedProvider(catalogEntry) ? catalogEntry : undefined;
156
- const builtinConfig = isBuiltinModelProviderConfig(config) ? config : undefined;
157
- const customConfig = isCustomModelProviderConfig(config) ? config : undefined;
158
- return (
159
- <ConfiguredProviderRow
160
- key={config.providerId}
161
- workspaceId={workspaceId}
162
- config={config}
163
- entry={entry}
164
- isDefault={config.providerId === defaultProviderId}
165
- onEdit={() => {
166
- if (entry && builtinConfig) {
167
- dispatchModal({type: 'edit-builtin', provider: entry, config: builtinConfig});
168
- } else if (customConfig) {
169
- dispatchModal({type: 'edit-custom', config: customConfig});
170
- }
171
- }}
172
- onChangeDefaultModel={() => {
173
- if (entry && builtinConfig)
174
- dispatchModal({
175
- type: 'change-default-model',
176
- provider: entry,
177
- config: builtinConfig,
178
- });
179
- }}
180
- onShowUsage={() => {
181
- if (entry && builtinConfig) {
182
- setPendingUsageTarget(null);
183
- dispatchModal({
184
- type: 'show-usage',
185
- providerId: entry.id,
186
- initialModel: builtinConfig.defaultModel,
187
- restoreFocusToConfiguredProviders: false,
188
- });
189
- } else if (customConfig) {
190
- setPendingUsageTarget(null);
191
- dispatchModal({
192
- type: 'show-usage',
193
- providerId: customConfig.providerId,
194
- initialModel: customConfig.defaultModel,
195
- restoreFocusToConfiguredProviders: false,
196
- });
197
- }
198
- }}
199
- />
200
- );
201
- })}
202
- </ul>
147
+ <Panel>
148
+ <PanelBody asChild>
149
+ <ul>
150
+ {configs.map((config) => {
151
+ const catalogEntry = providerById.get(config.providerId);
152
+ const entry =
153
+ catalogEntry && isSupportedProvider(catalogEntry) ? catalogEntry : undefined;
154
+ const builtinConfig = isBuiltinModelProviderConfig(config) ? config : undefined;
155
+ const customConfig = isCustomModelProviderConfig(config) ? config : undefined;
156
+ return (
157
+ <ConfiguredProviderRow
158
+ key={config.providerId}
159
+ workspaceId={workspaceId}
160
+ config={config}
161
+ entry={entry}
162
+ isDefault={config.providerId === defaultProviderId}
163
+ onEdit={() => {
164
+ if (entry && builtinConfig) {
165
+ dispatchModal({
166
+ type: 'edit-builtin',
167
+ provider: entry,
168
+ config: builtinConfig,
169
+ });
170
+ } else if (customConfig) {
171
+ dispatchModal({type: 'edit-custom', config: customConfig});
172
+ }
173
+ }}
174
+ onChangeDefaultModel={() => {
175
+ if (entry && builtinConfig)
176
+ dispatchModal({
177
+ type: 'change-default-model',
178
+ provider: entry,
179
+ config: builtinConfig,
180
+ });
181
+ }}
182
+ onShowUsage={() => {
183
+ if (entry && builtinConfig) {
184
+ setPendingUsageTarget(null);
185
+ dispatchModal({
186
+ type: 'show-usage',
187
+ providerId: entry.id,
188
+ initialModel: builtinConfig.defaultModel,
189
+ restoreFocusToConfiguredProviders: false,
190
+ });
191
+ } else if (customConfig) {
192
+ setPendingUsageTarget(null);
193
+ dispatchModal({
194
+ type: 'show-usage',
195
+ providerId: customConfig.providerId,
196
+ initialModel: customConfig.defaultModel,
197
+ restoreFocusToConfiguredProviders: false,
198
+ });
199
+ }
200
+ }}
201
+ />
202
+ );
203
+ })}
204
+ </ul>
205
+ </PanelBody>
206
+ </Panel>
203
207
  ) : null}
204
208
  </section>
205
209
 
206
210
  <section className="flex flex-col gap-group" aria-label="Available providers">
207
211
  <div className="flex flex-col gap-tight">
208
212
  <Header variant="h3">Available providers</Header>
209
- <Text size="sm" className="text-foreground-neutral-muted">
210
- Providers that can be configured for agent steps in this workspace.
211
- </Text>
212
213
  </div>
213
214
 
214
- {catalogQuery.isPending || configsQuery.isPending ? <ModelProviderGridSkeleton /> : null}
215
+ {catalogQuery.isPending || configsQuery.isPending ? (
216
+ <ModelProviderGridSkeleton label="Loading available providers" />
217
+ ) : null}
215
218
 
216
219
  {catalogQuery.isError && catalogQuery.data === undefined ? (
217
- <div className={cn(SURFACE_CLASS, 'px-row')}>
218
- <QueryLoadError query={catalogQuery} subject="model provider catalog" />
219
- </div>
220
+ <Panel>
221
+ <QueryLoadError query={catalogQuery} subject="model provider catalog" variant="panel" />
222
+ </Panel>
220
223
  ) : null}
221
224
 
222
225
  {configsLoaded ? (
@@ -234,9 +237,6 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
234
237
  <section className="flex flex-col gap-group" aria-label="Unsupported providers">
235
238
  <div className="flex flex-col gap-tight">
236
239
  <Header variant="h3">Unsupported providers</Header>
237
- <Text size="sm" className="text-foreground-neutral-muted">
238
- Providers that cannot be configured in this workspace yet.
239
- </Text>
240
240
  </div>
241
241
 
242
242
  {catalogQuery.isPending ? (
@@ -244,25 +244,35 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
244
244
  ) : null}
245
245
 
246
246
  {unsupportedProviders.length > 0 ? (
247
- <ul className={cn('divide-y divide-border-neutral-base', SURFACE_CLASS)}>
248
- {unsupportedProviders.map((entry) => (
249
- <li key={entry.id} className="flex items-start gap-cluster px-row py-row opacity-70">
250
- <Icon
251
- name="forbid2Line"
252
- className="mt-[2px] size-18 shrink-0 text-foreground-neutral-muted"
253
- aria-hidden
254
- />
255
- <div className="flex min-w-0 flex-1 flex-col gap-tight">
256
- <Text size="md" bold className="truncate">
257
- {entry.label}
258
- </Text>
259
- <Text size="sm" className="text-foreground-neutral-muted">
260
- {entry.unsupportedReason}
261
- </Text>
262
- </div>
263
- </li>
264
- ))}
265
- </ul>
247
+ <Panel>
248
+ <PanelBody asChild>
249
+ <ul>
250
+ {unsupportedProviders.map((entry) => (
251
+ <PanelRow
252
+ asChild
253
+ className="items-start justify-start gap-cluster opacity-70 hover:bg-background-neutral-base"
254
+ key={entry.id}
255
+ >
256
+ <li>
257
+ <Icon
258
+ name="forbid2Line"
259
+ className="mt-[2px] size-18 shrink-0 text-foreground-neutral-muted"
260
+ aria-hidden
261
+ />
262
+ <div className="flex min-w-0 flex-1 flex-col gap-tight">
263
+ <Text size="md" bold className="truncate">
264
+ {entry.label}
265
+ </Text>
266
+ <Text size="sm" className="text-foreground-neutral-muted">
267
+ {entry.unsupportedReason}
268
+ </Text>
269
+ </div>
270
+ </li>
271
+ </PanelRow>
272
+ ))}
273
+ </ul>
274
+ </PanelBody>
275
+ </Panel>
266
276
  ) : null}
267
277
  </section>
268
278
 
@@ -468,91 +478,93 @@ function ConfiguredProviderRow({
468
478
  }
469
479
 
470
480
  return (
471
- <li className="flex flex-col gap-inline px-row py-row transition-colors hover:bg-background-components-hover">
472
- <div className="flex items-center justify-between gap-cluster">
473
- <div className="flex min-w-0 items-center gap-inline">
474
- {isDefault ? (
475
- <>
476
- <Tooltip>
477
- <TooltipTrigger asChild>
478
- <span className="inline-flex size-16 shrink-0 items-center justify-center">
479
- <Icon
480
- name="starLine"
481
- className="size-16 text-foreground-neutral-muted"
482
- aria-hidden
483
- />
484
- </span>
485
- </TooltipTrigger>
486
- <TooltipContent>Default provider</TooltipContent>
487
- </Tooltip>
488
- <span className="sr-only">Default provider</span>
489
- </>
490
- ) : null}
491
- <Text size="md" bold className="truncate">
492
- {label}
493
- </Text>
494
- </div>
495
- <DropdownMenu>
496
- <DropdownMenuTrigger asChild>
497
- <IconButton
498
- size="sm"
499
- variant="transparent"
500
- icon="more2Line"
501
- aria-label={`Open ${label} provider actions`}
502
- />
503
- </DropdownMenuTrigger>
504
- <DropdownMenuContent align="end">
505
- {!isDefault ? (
481
+ <PanelRow asChild className="flex-col items-stretch gap-inline">
482
+ <li>
483
+ <div className="flex items-center justify-between gap-cluster">
484
+ <div className="flex min-w-0 items-center gap-inline">
485
+ {isDefault ? (
486
+ <>
487
+ <Tooltip>
488
+ <TooltipTrigger asChild>
489
+ <span className="inline-flex size-16 shrink-0 items-center justify-center">
490
+ <Icon
491
+ name="starLine"
492
+ className="size-16 text-foreground-neutral-muted"
493
+ aria-hidden
494
+ />
495
+ </span>
496
+ </TooltipTrigger>
497
+ <TooltipContent>Default provider</TooltipContent>
498
+ </Tooltip>
499
+ <span className="sr-only">Default provider</span>
500
+ </>
501
+ ) : null}
502
+ <Text size="md" bold className="truncate">
503
+ {label}
504
+ </Text>
505
+ </div>
506
+ <DropdownMenu>
507
+ <DropdownMenuTrigger asChild>
508
+ <IconButton
509
+ size="sm"
510
+ variant="transparent"
511
+ icon="more2Line"
512
+ aria-label={`Open ${label} provider actions`}
513
+ />
514
+ </DropdownMenuTrigger>
515
+ <DropdownMenuContent align="end">
516
+ {!isDefault ? (
517
+ <DropdownMenuItem
518
+ icon="starLine"
519
+ disabled={setDefault.isPending || (!entry && !customConfig)}
520
+ onSelect={() => {
521
+ void handleSetDefault();
522
+ }}
523
+ >
524
+ Set as default
525
+ </DropdownMenuItem>
526
+ ) : null}
527
+ {isBuiltinConfig ? (
528
+ <DropdownMenuItem
529
+ icon="settings3Line"
530
+ disabled={!entry}
531
+ onSelect={onChangeDefaultModel}
532
+ >
533
+ Change default model
534
+ </DropdownMenuItem>
535
+ ) : null}
536
+ <DropdownMenuItem icon="bookOpenLine" disabled={!canUse} onSelect={onShowUsage}>
537
+ View workflow example
538
+ </DropdownMenuItem>
539
+ <DropdownMenuItem icon="editLine" disabled={!canEdit} onSelect={onEdit}>
540
+ {customConfig ? 'Edit' : 'Edit credentials'}
541
+ </DropdownMenuItem>
506
542
  <DropdownMenuItem
507
- icon="starLine"
508
- disabled={setDefault.isPending || (!entry && !customConfig)}
543
+ icon="deleteBinLine"
509
544
  onSelect={() => {
510
- void handleSetDefault();
545
+ setDeleteOpen(true);
511
546
  }}
512
547
  >
513
- Set as default
514
- </DropdownMenuItem>
515
- ) : null}
516
- {isBuiltinConfig ? (
517
- <DropdownMenuItem
518
- icon="settings3Line"
519
- disabled={!entry}
520
- onSelect={onChangeDefaultModel}
521
- >
522
- Change default model
548
+ Delete
523
549
  </DropdownMenuItem>
524
- ) : null}
525
- <DropdownMenuItem icon="bookOpenLine" disabled={!canUse} onSelect={onShowUsage}>
526
- View workflow example
527
- </DropdownMenuItem>
528
- <DropdownMenuItem icon="editLine" disabled={!canEdit} onSelect={onEdit}>
529
- {customConfig ? 'Edit' : 'Edit credentials'}
530
- </DropdownMenuItem>
531
- <DropdownMenuItem
532
- icon="deleteBinLine"
533
- onSelect={() => {
534
- setDeleteOpen(true);
535
- }}
536
- >
537
- Delete
538
- </DropdownMenuItem>
539
- </DropdownMenuContent>
540
- </DropdownMenu>
541
- </div>
542
- {defaultError ? (
543
- <Callout role="alert" type="error">
544
- <Text size="sm">{defaultError}</Text>
545
- </Callout>
546
- ) : null}
547
- <DeleteModelProviderDialog
548
- open={deleteOpen}
549
- onOpenChange={handleDeleteOpenChange}
550
- label={label}
551
- errorMessage={deleteError}
552
- isLoading={deleteConfig.isPending}
553
- onDelete={handleDelete}
554
- />
555
- </li>
550
+ </DropdownMenuContent>
551
+ </DropdownMenu>
552
+ </div>
553
+ {defaultError ? (
554
+ <Callout role="alert" type="error">
555
+ <Text size="sm">{defaultError}</Text>
556
+ </Callout>
557
+ ) : null}
558
+ <DeleteModelProviderDialog
559
+ open={deleteOpen}
560
+ onOpenChange={handleDeleteOpenChange}
561
+ label={label}
562
+ errorMessage={deleteError}
563
+ isLoading={deleteConfig.isPending}
564
+ onDelete={handleDelete}
565
+ />
566
+ </li>
567
+ </PanelRow>
556
568
  );
557
569
  }
558
570
 
@@ -602,31 +614,27 @@ function DeleteModelProviderDialog({
602
614
 
603
615
  function ModelProviderRowsSkeleton({label}: {label: string}) {
604
616
  return (
605
- <ul
606
- role="status"
607
- aria-label={label}
608
- className={cn('divide-y divide-border-neutral-base', SURFACE_CLASS)}
609
- >
610
- {[0, 1, 2].map((row) => (
611
- <li key={row} className="flex items-center gap-cluster px-row py-row">
612
- <Skeleton className="size-32 shrink-0" />
613
- <div className="flex min-w-0 flex-1 flex-col gap-inline">
614
- <Skeleton className="h-16 w-120" />
615
- <Skeleton className="h-14 w-180" />
616
- </div>
617
- <Skeleton className="h-28 w-96 shrink-0" />
618
- </li>
619
- ))}
620
- </ul>
621
- );
622
- }
623
-
624
- function ModelProviderGridSkeleton() {
625
- return (
626
- <div role="status" aria-label="Loading available providers" className={PROVIDER_GRID_CLASS}>
627
- {[0, 1, 2, 3].map((card) => (
628
- <Skeleton key={card} className="h-136 w-full" />
629
- ))}
630
- </div>
617
+ <Panel>
618
+ <PanelBody asChild>
619
+ <ul role="status" aria-label={label}>
620
+ {[0, 1, 2].map((row) => (
621
+ <PanelRow
622
+ asChild
623
+ className="justify-start gap-cluster hover:bg-background-neutral-base"
624
+ key={row}
625
+ >
626
+ <li>
627
+ <Skeleton className="size-32 shrink-0" />
628
+ <div className="flex min-w-0 flex-1 flex-col gap-inline">
629
+ <Skeleton className="h-16 w-120" />
630
+ <Skeleton className="h-14 w-180" />
631
+ </div>
632
+ <Skeleton className="h-28 w-96 shrink-0" />
633
+ </li>
634
+ </PanelRow>
635
+ ))}
636
+ </ul>
637
+ </PanelBody>
638
+ </Panel>
631
639
  );
632
640
  }
@@ -2,15 +2,14 @@ import {QueryLoadError} from '@shipfox/client-ui';
2
2
  import {Button} from '@shipfox/react-ui/button';
3
3
  import {Callout} from '@shipfox/react-ui/callout';
4
4
  import {EmptyState} from '@shipfox/react-ui/empty-state';
5
- import {Icon} from '@shipfox/react-ui/icon';
6
5
  import {Modal, ModalContent, ModalHeader, ModalTitle} from '@shipfox/react-ui/modal';
7
- import {Skeleton} from '@shipfox/react-ui/skeleton';
6
+ import {Panel, PanelBody, PanelCell, PanelCellAction, PanelGrid} from '@shipfox/react-ui/panel';
8
7
  import {toast} from '@shipfox/react-ui/toast';
9
8
  import {Header, Text} from '@shipfox/react-ui/typography';
10
- import {cn} from '@shipfox/react-ui/utils';
11
9
  import {useEffect, useMemo, useReducer} from 'react';
12
- import {AvailableProvidersGrid, PROVIDER_GRID_CLASS} from '#components/available-providers-grid.js';
10
+ import {AvailableProvidersGrid} from '#components/available-providers-grid.js';
13
11
  import {modelProviderConfigErrorToFormError} from '#components/form-errors.js';
12
+ import {ModelProviderGridSkeleton} from '#components/model-provider-grid-skeleton.js';
14
13
  import {ModelProviderTestAndSaveForm} from '#components/test-and-save-form.js';
15
14
  import {DEFAULT_HARNESS, harnessSupportsProvider, listHarnesses} from '#core/harness-policy.js';
16
15
  import type {HarnessDescriptor, HarnessId, SupportedProvider} from '#core/models.js';
@@ -22,9 +21,6 @@ import {
22
21
  } from '#hooks/api/model-providers.js';
23
22
  import {dismissModelProviderOnboarding} from '#state/model-provider-onboarding.js';
24
23
 
25
- const SURFACE_CLASS =
26
- 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
27
-
28
24
  export function ModelProviderOnboardingPage({
29
25
  workspaceId,
30
26
  onSkip,
@@ -104,7 +100,7 @@ export function ModelProviderOnboardingPage({
104
100
  : 'model-provider-provider-step';
105
101
 
106
102
  return (
107
- <div className="mx-auto flex w-full flex-col gap-section">
103
+ <div className="flex w-full flex-col gap-section">
108
104
  <header className="flex flex-col gap-cluster">
109
105
  <div className="flex items-start justify-between gap-group max-[520px]:flex-col max-[520px]:gap-inline">
110
106
  <Header id={headingId} variant="h1" tabIndex={-1} className="outline-none">
@@ -216,42 +212,32 @@ export function ModelProviderOnboardingPage({
216
212
 
217
213
  function HarnessPicker({onSelect}: {onSelect: (harness: HarnessId) => void}) {
218
214
  return (
219
- <ul className={PROVIDER_GRID_CLASS} aria-label="Agent harnesses">
220
- {listHarnesses().map((harness) => (
221
- <HarnessCard key={harness.id} harness={harness} onChoose={() => onSelect(harness.id)} />
222
- ))}
223
- </ul>
215
+ <Panel>
216
+ <PanelBody>
217
+ <PanelGrid aria-label="Agent harnesses">
218
+ {listHarnesses().map((harness) => (
219
+ <HarnessCard key={harness.id} harness={harness} onChoose={() => onSelect(harness.id)} />
220
+ ))}
221
+ </PanelGrid>
222
+ </PanelBody>
223
+ </Panel>
224
224
  );
225
225
  }
226
226
 
227
227
  function HarnessCard({harness, onChoose}: {harness: HarnessDescriptor; onChoose: () => void}) {
228
228
  return (
229
- <li>
230
- <button
231
- type="button"
232
- className={cn(
233
- 'group block w-full cursor-pointer p-panel-compact text-left outline-none transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus',
234
- SURFACE_CLASS,
235
- )}
236
- aria-label={`Choose ${harness.label}`}
237
- onClick={onChoose}
238
- >
239
- <div className="flex min-w-0 items-center justify-between gap-cluster">
240
- <div className="flex min-w-0 flex-col gap-tight">
241
- <Text size="md" bold className="min-w-0 truncate">
242
- {harness.label}
243
- </Text>
244
- <Text size="sm" className="text-foreground-neutral-muted">
245
- {harness.description}
246
- </Text>
247
- </div>
248
- <div className="flex shrink-0 items-center gap-tight text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
249
- <Text size="sm">Choose</Text>
250
- <Icon name="chevronRight" className="size-16" />
251
- </div>
252
- </div>
253
- </button>
254
- </li>
229
+ <PanelCell>
230
+ <PanelCellAction action="Choose" aria-label={`Choose ${harness.label}`} onClick={onChoose}>
231
+ <span className="flex min-w-0 flex-col gap-tight">
232
+ <Text as="span" size="md" bold className="min-w-0 truncate">
233
+ {harness.label}
234
+ </Text>
235
+ <Text as="span" size="sm" className="text-foreground-neutral-muted">
236
+ {harness.description}
237
+ </Text>
238
+ </span>
239
+ </PanelCellAction>
240
+ </PanelCell>
255
241
  );
256
242
  }
257
243
 
@@ -265,11 +251,7 @@ function ModelProviderPicker({
265
251
  onSelect: (entry: SupportedProvider) => void;
266
252
  }) {
267
253
  if (catalogQuery.isPending) {
268
- return (
269
- <div role="status" aria-busy="true" aria-label="Loading model providers">
270
- <ModelProviderGridSkeleton />
271
- </div>
272
- );
254
+ return <ModelProviderGridSkeleton label="Loading model providers" />;
273
255
  }
274
256
 
275
257
  if (catalogQuery.isError && catalogQuery.data === undefined) {
@@ -288,13 +270,3 @@ function ModelProviderPicker({
288
270
 
289
271
  return <AvailableProvidersGrid entries={supportedProviders} onSelect={onSelect} />;
290
272
  }
291
-
292
- function ModelProviderGridSkeleton() {
293
- return (
294
- <div className={PROVIDER_GRID_CLASS}>
295
- {[0, 1, 2, 3].map((card) => (
296
- <Skeleton key={card} className="h-136 w-full" />
297
- ))}
298
- </div>
299
- );
300
- }
@@ -1,4 +1,5 @@
1
1
  import {defineRoute, useActiveWorkspace} from '@shipfox/client-shell/runtime';
2
+ import {Header} from '@shipfox/react-ui/typography';
2
3
  import {WorkspaceHarnessesSection, WorkspaceModelProvidersSection} from '#index.js';
3
4
 
4
5
  export default defineRoute({
@@ -6,9 +7,12 @@ export default defineRoute({
6
7
  component: () => {
7
8
  const workspace = useActiveWorkspace();
8
9
  return (
9
- <div className="flex flex-col gap-region">
10
- <WorkspaceHarnessesSection workspaceId={workspace.id} />
11
- <WorkspaceModelProvidersSection workspaceId={workspace.id} />
10
+ <div className="flex min-w-0 flex-col gap-section">
11
+ <Header variant="h1">Agents</Header>
12
+ <div className="flex flex-col gap-region">
13
+ <WorkspaceHarnessesSection workspaceId={workspace.id} />
14
+ <WorkspaceModelProvidersSection workspaceId={workspace.id} />
15
+ </div>
12
16
  </div>
13
17
  );
14
18
  },