@shipfox/client-agent 21.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 (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -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 +147 -134
  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 +253 -245
  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/tsconfig.test.tsbuildinfo +1 -1
  27. package/package.json +4 -4
  28. package/src/components/add-custom-provider-card.tsx +9 -23
  29. package/src/components/available-provider-card.tsx +9 -23
  30. package/src/components/available-providers-grid.tsx +36 -29
  31. package/src/components/harnesses-section.tsx +116 -106
  32. package/src/components/model-provider-grid-skeleton.tsx +21 -0
  33. package/src/components/model-providers-section.tsx +208 -191
  34. package/src/pages/model-provider-onboarding-page.tsx +26 -54
  35. 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;
@@ -132,74 +130,83 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
132
130
  ) : null}
133
131
 
134
132
  {configsQuery.isError && configsQuery.data === undefined ? (
135
- <div className={cn(SURFACE_CLASS, 'px-row')}>
136
- <QueryLoadError query={configsQuery} subject="model provider configs" />
137
- </div>
133
+ <Panel>
134
+ <QueryLoadError query={configsQuery} subject="model provider configs" variant="panel" />
135
+ </Panel>
138
136
  ) : null}
139
137
 
140
138
  {configsQuery.data !== undefined && configs.length === 0 ? (
141
- <div className={cn(SURFACE_CLASS, 'px-row')}>
139
+ <Panel>
142
140
  <EmptyState
143
141
  icon="key2Line"
144
142
  title="No providers configured"
145
143
  description="Configure a provider below to run agent steps with workspace-managed credentials."
144
+ variant="panel"
146
145
  />
147
- </div>
146
+ </Panel>
148
147
  ) : null}
149
148
 
150
149
  {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>
150
+ <Panel>
151
+ <PanelBody asChild>
152
+ <ul>
153
+ {configs.map((config) => {
154
+ const catalogEntry = providerById.get(config.providerId);
155
+ const entry =
156
+ catalogEntry && isSupportedProvider(catalogEntry) ? catalogEntry : undefined;
157
+ const builtinConfig = isBuiltinModelProviderConfig(config) ? config : undefined;
158
+ const customConfig = isCustomModelProviderConfig(config) ? config : undefined;
159
+ return (
160
+ <ConfiguredProviderRow
161
+ key={config.providerId}
162
+ workspaceId={workspaceId}
163
+ config={config}
164
+ entry={entry}
165
+ isDefault={config.providerId === defaultProviderId}
166
+ onEdit={() => {
167
+ if (entry && builtinConfig) {
168
+ dispatchModal({
169
+ type: 'edit-builtin',
170
+ provider: entry,
171
+ config: builtinConfig,
172
+ });
173
+ } else if (customConfig) {
174
+ dispatchModal({type: 'edit-custom', config: customConfig});
175
+ }
176
+ }}
177
+ onChangeDefaultModel={() => {
178
+ if (entry && builtinConfig)
179
+ dispatchModal({
180
+ type: 'change-default-model',
181
+ provider: entry,
182
+ config: builtinConfig,
183
+ });
184
+ }}
185
+ onShowUsage={() => {
186
+ if (entry && builtinConfig) {
187
+ setPendingUsageTarget(null);
188
+ dispatchModal({
189
+ type: 'show-usage',
190
+ providerId: entry.id,
191
+ initialModel: builtinConfig.defaultModel,
192
+ restoreFocusToConfiguredProviders: false,
193
+ });
194
+ } else if (customConfig) {
195
+ setPendingUsageTarget(null);
196
+ dispatchModal({
197
+ type: 'show-usage',
198
+ providerId: customConfig.providerId,
199
+ initialModel: customConfig.defaultModel,
200
+ restoreFocusToConfiguredProviders: false,
201
+ });
202
+ }
203
+ }}
204
+ />
205
+ );
206
+ })}
207
+ </ul>
208
+ </PanelBody>
209
+ </Panel>
203
210
  ) : null}
204
211
  </section>
205
212
 
@@ -211,12 +218,14 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
211
218
  </Text>
212
219
  </div>
213
220
 
214
- {catalogQuery.isPending || configsQuery.isPending ? <ModelProviderGridSkeleton /> : null}
221
+ {catalogQuery.isPending || configsQuery.isPending ? (
222
+ <ModelProviderGridSkeleton label="Loading available providers" />
223
+ ) : null}
215
224
 
216
225
  {catalogQuery.isError && catalogQuery.data === undefined ? (
217
- <div className={cn(SURFACE_CLASS, 'px-row')}>
218
- <QueryLoadError query={catalogQuery} subject="model provider catalog" />
219
- </div>
226
+ <Panel>
227
+ <QueryLoadError query={catalogQuery} subject="model provider catalog" variant="panel" />
228
+ </Panel>
220
229
  ) : null}
221
230
 
222
231
  {configsLoaded ? (
@@ -244,25 +253,35 @@ export function WorkspaceModelProvidersSection({workspaceId}: {workspaceId: stri
244
253
  ) : null}
245
254
 
246
255
  {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>
256
+ <Panel>
257
+ <PanelBody asChild>
258
+ <ul>
259
+ {unsupportedProviders.map((entry) => (
260
+ <PanelRow
261
+ asChild
262
+ className="items-start justify-start gap-cluster opacity-70 hover:bg-background-neutral-base"
263
+ key={entry.id}
264
+ >
265
+ <li>
266
+ <Icon
267
+ name="forbid2Line"
268
+ className="mt-[2px] size-18 shrink-0 text-foreground-neutral-muted"
269
+ aria-hidden
270
+ />
271
+ <div className="flex min-w-0 flex-1 flex-col gap-tight">
272
+ <Text size="md" bold className="truncate">
273
+ {entry.label}
274
+ </Text>
275
+ <Text size="sm" className="text-foreground-neutral-muted">
276
+ {entry.unsupportedReason}
277
+ </Text>
278
+ </div>
279
+ </li>
280
+ </PanelRow>
281
+ ))}
282
+ </ul>
283
+ </PanelBody>
284
+ </Panel>
266
285
  ) : null}
267
286
  </section>
268
287
 
@@ -468,91 +487,93 @@ function ConfiguredProviderRow({
468
487
  }
469
488
 
470
489
  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 ? (
490
+ <PanelRow asChild className="flex-col items-stretch gap-inline">
491
+ <li>
492
+ <div className="flex items-center justify-between gap-cluster">
493
+ <div className="flex min-w-0 items-center gap-inline">
494
+ {isDefault ? (
495
+ <>
496
+ <Tooltip>
497
+ <TooltipTrigger asChild>
498
+ <span className="inline-flex size-16 shrink-0 items-center justify-center">
499
+ <Icon
500
+ name="starLine"
501
+ className="size-16 text-foreground-neutral-muted"
502
+ aria-hidden
503
+ />
504
+ </span>
505
+ </TooltipTrigger>
506
+ <TooltipContent>Default provider</TooltipContent>
507
+ </Tooltip>
508
+ <span className="sr-only">Default provider</span>
509
+ </>
510
+ ) : null}
511
+ <Text size="md" bold className="truncate">
512
+ {label}
513
+ </Text>
514
+ </div>
515
+ <DropdownMenu>
516
+ <DropdownMenuTrigger asChild>
517
+ <IconButton
518
+ size="sm"
519
+ variant="transparent"
520
+ icon="more2Line"
521
+ aria-label={`Open ${label} provider actions`}
522
+ />
523
+ </DropdownMenuTrigger>
524
+ <DropdownMenuContent align="end">
525
+ {!isDefault ? (
526
+ <DropdownMenuItem
527
+ icon="starLine"
528
+ disabled={setDefault.isPending || (!entry && !customConfig)}
529
+ onSelect={() => {
530
+ void handleSetDefault();
531
+ }}
532
+ >
533
+ Set as default
534
+ </DropdownMenuItem>
535
+ ) : null}
536
+ {isBuiltinConfig ? (
537
+ <DropdownMenuItem
538
+ icon="settings3Line"
539
+ disabled={!entry}
540
+ onSelect={onChangeDefaultModel}
541
+ >
542
+ Change default model
543
+ </DropdownMenuItem>
544
+ ) : null}
545
+ <DropdownMenuItem icon="bookOpenLine" disabled={!canUse} onSelect={onShowUsage}>
546
+ View workflow example
547
+ </DropdownMenuItem>
548
+ <DropdownMenuItem icon="editLine" disabled={!canEdit} onSelect={onEdit}>
549
+ {customConfig ? 'Edit' : 'Edit credentials'}
550
+ </DropdownMenuItem>
506
551
  <DropdownMenuItem
507
- icon="starLine"
508
- disabled={setDefault.isPending || (!entry && !customConfig)}
552
+ icon="deleteBinLine"
509
553
  onSelect={() => {
510
- void handleSetDefault();
554
+ setDeleteOpen(true);
511
555
  }}
512
556
  >
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
557
+ Delete
523
558
  </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>
559
+ </DropdownMenuContent>
560
+ </DropdownMenu>
561
+ </div>
562
+ {defaultError ? (
563
+ <Callout role="alert" type="error">
564
+ <Text size="sm">{defaultError}</Text>
565
+ </Callout>
566
+ ) : null}
567
+ <DeleteModelProviderDialog
568
+ open={deleteOpen}
569
+ onOpenChange={handleDeleteOpenChange}
570
+ label={label}
571
+ errorMessage={deleteError}
572
+ isLoading={deleteConfig.isPending}
573
+ onDelete={handleDelete}
574
+ />
575
+ </li>
576
+ </PanelRow>
556
577
  );
557
578
  }
558
579
 
@@ -602,31 +623,27 @@ function DeleteModelProviderDialog({
602
623
 
603
624
  function ModelProviderRowsSkeleton({label}: {label: string}) {
604
625
  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>
626
+ <Panel>
627
+ <PanelBody asChild>
628
+ <ul role="status" aria-label={label}>
629
+ {[0, 1, 2].map((row) => (
630
+ <PanelRow
631
+ asChild
632
+ className="justify-start gap-cluster hover:bg-background-neutral-base"
633
+ key={row}
634
+ >
635
+ <li>
636
+ <Skeleton className="size-32 shrink-0" />
637
+ <div className="flex min-w-0 flex-1 flex-col gap-inline">
638
+ <Skeleton className="h-16 w-120" />
639
+ <Skeleton className="h-14 w-180" />
640
+ </div>
641
+ <Skeleton className="h-28 w-96 shrink-0" />
642
+ </li>
643
+ </PanelRow>
644
+ ))}
645
+ </ul>
646
+ </PanelBody>
647
+ </Panel>
631
648
  );
632
649
  }
@@ -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
- }