@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-agent",
3
3
  "license": "MIT",
4
- "version": "21.0.0",
4
+ "version": "22.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -30,9 +30,9 @@
30
30
  "@tanstack/react-query": "^5.101.0",
31
31
  "@shipfox/api-agent-dto": "12.2.0",
32
32
  "@shipfox/client-api": "6.0.1",
33
- "@shipfox/client-shell": "21.0.0",
34
- "@shipfox/client-ui": "21.0.0",
35
- "@shipfox/react-ui": "2.0.0"
33
+ "@shipfox/client-shell": "22.0.1",
34
+ "@shipfox/client-ui": "22.0.0",
35
+ "@shipfox/react-ui": "2.1.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tanstack/react-query": "^5.101.0",
@@ -1,32 +1,18 @@
1
- import {Icon} from '@shipfox/react-ui/icon';
1
+ import {PanelCell, PanelCellAction} from '@shipfox/react-ui/panel';
2
2
  import {Text} from '@shipfox/react-ui/typography';
3
- import {cn} from '@shipfox/react-ui/utils';
4
-
5
- const SURFACE_CLASS =
6
- 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
7
3
 
8
4
  export function AddCustomProviderCard({onConfigure}: {onConfigure: () => void}) {
9
5
  return (
10
- <li>
11
- <button
12
- type="button"
13
- className={cn(
14
- 'group block h-full w-full cursor-pointer p-panel-compact text-left outline-none transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus',
15
- SURFACE_CLASS,
16
- )}
6
+ <PanelCell>
7
+ <PanelCellAction
8
+ action="Configure"
17
9
  aria-label="Configure custom provider"
18
10
  onClick={onConfigure}
19
11
  >
20
- <div className="flex min-w-0 items-center justify-between gap-cluster">
21
- <Text size="md" bold className="min-w-0 truncate">
22
- Custom
23
- </Text>
24
- <div className="flex shrink-0 items-center gap-tight text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
25
- <Text size="sm">Configure</Text>
26
- <Icon name="chevronRight" className="size-16" />
27
- </div>
28
- </div>
29
- </button>
30
- </li>
12
+ <Text as="span" size="md" bold className="min-w-0 truncate">
13
+ Custom
14
+ </Text>
15
+ </PanelCellAction>
16
+ </PanelCell>
31
17
  );
32
18
  }
@@ -1,11 +1,7 @@
1
- import {Icon} from '@shipfox/react-ui/icon';
1
+ import {PanelCell, PanelCellAction} from '@shipfox/react-ui/panel';
2
2
  import {Text} from '@shipfox/react-ui/typography';
3
- import {cn} from '@shipfox/react-ui/utils';
4
3
  import type {SupportedProvider} from '#core/models.js';
5
4
 
6
- const SURFACE_CLASS =
7
- 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
8
-
9
5
  export function AvailableProviderCard({
10
6
  entry,
11
7
  onConfigure,
@@ -14,26 +10,16 @@ export function AvailableProviderCard({
14
10
  onConfigure: () => void;
15
11
  }) {
16
12
  return (
17
- <li>
18
- <button
19
- type="button"
20
- className={cn(
21
- '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',
22
- SURFACE_CLASS,
23
- )}
13
+ <PanelCell>
14
+ <PanelCellAction
15
+ action="Configure"
24
16
  aria-label={`Configure ${entry.label}`}
25
17
  onClick={onConfigure}
26
18
  >
27
- <div className="flex min-w-0 items-center justify-between gap-cluster">
28
- <Text size="md" bold className="min-w-0 truncate">
29
- {entry.label}
30
- </Text>
31
- <div className="flex shrink-0 items-center gap-tight text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
32
- <Text size="sm">Configure</Text>
33
- <Icon name="chevronRight" className="size-16" />
34
- </div>
35
- </div>
36
- </button>
37
- </li>
19
+ <Text as="span" size="md" bold className="min-w-0 truncate">
20
+ {entry.label}
21
+ </Text>
22
+ </PanelCellAction>
23
+ </PanelCell>
38
24
  );
39
25
  }
@@ -2,14 +2,13 @@ import {Button} from '@shipfox/react-ui/button';
2
2
  import {EmptyState} from '@shipfox/react-ui/empty-state';
3
3
  import {Icon} from '@shipfox/react-ui/icon';
4
4
  import {Input} from '@shipfox/react-ui/input';
5
+ import {Panel, PanelBody, PanelGrid, PanelHeader} from '@shipfox/react-ui/panel';
5
6
  import type {ReactNode} from 'react';
6
7
  import {useMemo, useRef, useState} from 'react';
7
8
  import type {SupportedProvider} from '#core/models.js';
8
9
  import {providerMatchesSearch} from '#core/provider-policy.js';
9
10
  import {AvailableProviderCard} from './available-provider-card.js';
10
11
 
11
- export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1';
12
-
13
12
  const SEARCH_VISIBILITY_THRESHOLD = 8;
14
13
  const MAX_ECHOED_QUERY_LENGTH = 40;
15
14
 
@@ -45,38 +44,46 @@ export function AvailableProvidersGrid<TEntry extends SupportedProvider>({
45
44
  }
46
45
 
47
46
  return (
48
- <div className="flex flex-col gap-cluster">
49
- {showSearch ? (
50
- <Input
51
- ref={inputRef}
52
- type="search"
53
- aria-label="Search providers"
54
- placeholder="Search providers..."
55
- value={search}
56
- iconLeft={<Icon name="searchLine" className="size-16 text-foreground-neutral-muted" />}
57
- onChange={(event) => setSearch(event.target.value)}
58
- />
59
- ) : null}
60
-
61
- {visibleCount > 0 ? (
62
- <ul className={PROVIDER_GRID_CLASS} aria-label={providerListLabel}>
63
- {filteredEntries.map((entry) => (
64
- <AvailableProviderCard
65
- key={entry.id}
66
- entry={entry}
67
- onConfigure={() => onSelect(entry)}
47
+ <>
48
+ <Panel>
49
+ {showSearch ? (
50
+ <PanelHeader>
51
+ <Input
52
+ ref={inputRef}
53
+ type="search"
54
+ aria-label="Search providers"
55
+ placeholder="Search providers..."
56
+ value={search}
57
+ iconLeft={
58
+ <Icon name="searchLine" className="size-16 text-foreground-neutral-muted" />
59
+ }
60
+ onChange={(event) => setSearch(event.target.value)}
68
61
  />
69
- ))}
70
- {trailingCardVisible ? trailingCard : null}
71
- </ul>
72
- ) : (
73
- <NoProviderSearchResults search={trimmedSearch} onClear={clearSearch} />
74
- )}
62
+ </PanelHeader>
63
+ ) : null}
64
+
65
+ <PanelBody>
66
+ {visibleCount > 0 ? (
67
+ <PanelGrid aria-label={providerListLabel}>
68
+ {filteredEntries.map((entry) => (
69
+ <AvailableProviderCard
70
+ key={entry.id}
71
+ entry={entry}
72
+ onConfigure={() => onSelect(entry)}
73
+ />
74
+ ))}
75
+ {trailingCardVisible ? trailingCard : null}
76
+ </PanelGrid>
77
+ ) : (
78
+ <NoProviderSearchResults search={trimmedSearch} onClear={clearSearch} />
79
+ )}
80
+ </PanelBody>
81
+ </Panel>
75
82
 
76
83
  <p role="status" aria-live="polite" className="sr-only">
77
84
  {resultCountText}
78
85
  </p>
79
- </div>
86
+ </>
80
87
  );
81
88
  }
82
89
 
@@ -8,11 +8,11 @@ import {
8
8
  DropdownMenuTrigger,
9
9
  } from '@shipfox/react-ui/dropdown-menu';
10
10
  import {Icon} from '@shipfox/react-ui/icon';
11
+ import {Panel, PanelBody, PanelRow} from '@shipfox/react-ui/panel';
11
12
  import {Skeleton} from '@shipfox/react-ui/skeleton';
12
13
  import {toast} from '@shipfox/react-ui/toast';
13
14
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
14
15
  import {Header, Text} from '@shipfox/react-ui/typography';
15
- import {cn} from '@shipfox/react-ui/utils';
16
16
  import {useRef, useState} from 'react';
17
17
  import {DEFAULT_HARNESS, listHarnesses} from '#core/harness-policy.js';
18
18
  import type {HarnessDescriptor, HarnessId} from '#core/models.js';
@@ -23,9 +23,6 @@ import {
23
23
  import {modelProviderConfigErrorToFormError} from './form-errors.js';
24
24
  import {isHarnessAvailable} from './harness-availability.js';
25
25
 
26
- const SURFACE_CLASS =
27
- 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
28
-
29
26
  export function WorkspaceHarnessesSection({workspaceId}: {workspaceId: string}) {
30
27
  const configsQuery = useModelProviderConfigsQuery(workspaceId);
31
28
  const setDefaultHarness = useSetDefaultHarnessMutation();
@@ -87,37 +84,39 @@ export function WorkspaceHarnessesSection({workspaceId}: {workspaceId: string})
87
84
  <section className="flex flex-col gap-group" aria-label="Harnesses">
88
85
  <div className="flex flex-col gap-tight">
89
86
  <Header variant="h3">Harnesses</Header>
90
- <Text size="sm" className="text-foreground-neutral-muted">
91
- Harnesses available to run agent steps in this workspace.
92
- </Text>
93
87
  </div>
94
88
 
95
89
  {configsQuery.isPending ? <HarnessRowsSkeleton /> : null}
96
90
 
97
91
  {configsQuery.isError && configsQuery.data === undefined ? (
98
- <div className={cn(SURFACE_CLASS, 'px-row')}>
99
- <QueryLoadError query={configsQuery} subject="harnesses" />
100
- </div>
92
+ <Panel>
93
+ <QueryLoadError query={configsQuery} subject="harnesses" variant="panel" />
94
+ </Panel>
101
95
  ) : null}
102
96
 
103
97
  {configsQuery.data !== undefined ? (
104
- <ul className={cn('divide-y divide-border-neutral-base', SURFACE_CLASS)}>
105
- {listHarnesses().map((harness) => (
106
- <HarnessRow
107
- key={harness.id}
108
- harness={harness}
109
- isDefault={harness.id === defaultHarnessId}
110
- isAvailable={isHarnessAvailable(harness, configs)}
111
- isSettingDefault={pendingDefaultHarness?.workspaceId === workspaceId}
112
- defaultError={
113
- defaultError?.workspaceId === workspaceId && defaultError.harnessId === harness.id
114
- ? defaultError.message
115
- : undefined
116
- }
117
- onSetDefault={handleSetDefault}
118
- />
119
- ))}
120
- </ul>
98
+ <Panel>
99
+ <PanelBody asChild>
100
+ <ul>
101
+ {listHarnesses().map((harness) => (
102
+ <HarnessRow
103
+ key={harness.id}
104
+ harness={harness}
105
+ isDefault={harness.id === defaultHarnessId}
106
+ isAvailable={isHarnessAvailable(harness, configs)}
107
+ isSettingDefault={pendingDefaultHarness?.workspaceId === workspaceId}
108
+ defaultError={
109
+ defaultError?.workspaceId === workspaceId &&
110
+ defaultError.harnessId === harness.id
111
+ ? defaultError.message
112
+ : undefined
113
+ }
114
+ onSetDefault={handleSetDefault}
115
+ />
116
+ ))}
117
+ </ul>
118
+ </PanelBody>
119
+ </Panel>
121
120
  ) : null}
122
121
  </section>
123
122
  );
@@ -141,78 +140,80 @@ function HarnessRow({
141
140
  const unavailableCopy = harnessUnavailableCopy(isDefault);
142
141
 
143
142
  return (
144
- <li className="flex flex-col gap-inline px-row py-row transition-colors hover:bg-background-components-hover">
145
- <div className="flex items-center justify-between gap-cluster">
146
- <div className="flex min-w-0 items-center gap-inline">
147
- {isDefault ? (
148
- <>
149
- <Tooltip>
150
- <TooltipTrigger asChild>
151
- <span className="inline-flex size-16 shrink-0 items-center justify-center">
152
- <Icon
153
- name="starLine"
154
- className="size-16 text-foreground-neutral-muted"
155
- aria-hidden
156
- />
157
- </span>
158
- </TooltipTrigger>
159
- <TooltipContent>Default harness</TooltipContent>
160
- </Tooltip>
161
- <span className="sr-only">Default harness</span>
162
- </>
163
- ) : null}
164
- <Text size="md" bold className="truncate">
165
- {harness.label}
166
- </Text>
167
- {!isAvailable ? (
168
- <>
169
- <Tooltip>
170
- <TooltipTrigger asChild>
171
- <span className="inline-flex size-16 shrink-0 items-center justify-center">
172
- <Icon
173
- name="errorWarningLine"
174
- className="size-16 text-foreground-warning-base"
175
- aria-hidden
176
- />
177
- </span>
178
- </TooltipTrigger>
179
- <TooltipContent>{unavailableCopy}</TooltipContent>
180
- </Tooltip>
181
- <span className="sr-only">{unavailableCopy}</span>
182
- </>
143
+ <PanelRow asChild className="flex-col items-stretch gap-inline">
144
+ <li>
145
+ <div className="flex items-center justify-between gap-cluster">
146
+ <div className="flex min-w-0 items-center gap-inline">
147
+ {isDefault ? (
148
+ <>
149
+ <Tooltip>
150
+ <TooltipTrigger asChild>
151
+ <span className="inline-flex size-16 shrink-0 items-center justify-center">
152
+ <Icon
153
+ name="starLine"
154
+ className="size-16 text-foreground-neutral-muted"
155
+ aria-hidden
156
+ />
157
+ </span>
158
+ </TooltipTrigger>
159
+ <TooltipContent>Default harness</TooltipContent>
160
+ </Tooltip>
161
+ <span className="sr-only">Default harness</span>
162
+ </>
163
+ ) : null}
164
+ <Text size="md" bold className="truncate">
165
+ {harness.label}
166
+ </Text>
167
+ {!isAvailable ? (
168
+ <>
169
+ <Tooltip>
170
+ <TooltipTrigger asChild>
171
+ <span className="inline-flex size-16 shrink-0 items-center justify-center">
172
+ <Icon
173
+ name="errorWarningLine"
174
+ className="size-16 text-foreground-warning-base"
175
+ aria-hidden
176
+ />
177
+ </span>
178
+ </TooltipTrigger>
179
+ <TooltipContent>{unavailableCopy}</TooltipContent>
180
+ </Tooltip>
181
+ <span className="sr-only">{unavailableCopy}</span>
182
+ </>
183
+ ) : null}
184
+ </div>
185
+ {!isDefault ? (
186
+ <DropdownMenu>
187
+ <DropdownMenuTrigger asChild>
188
+ <IconButton
189
+ size="sm"
190
+ variant="transparent"
191
+ icon="more2Line"
192
+ disabled={isSettingDefault}
193
+ aria-label={`Open ${harness.label} harness actions`}
194
+ />
195
+ </DropdownMenuTrigger>
196
+ <DropdownMenuContent align="end">
197
+ <DropdownMenuItem
198
+ icon="starLine"
199
+ disabled={isSettingDefault || !isAvailable}
200
+ onSelect={() => {
201
+ onSetDefault(harness);
202
+ }}
203
+ >
204
+ Set as default
205
+ </DropdownMenuItem>
206
+ </DropdownMenuContent>
207
+ </DropdownMenu>
183
208
  ) : null}
184
209
  </div>
185
- {!isDefault ? (
186
- <DropdownMenu>
187
- <DropdownMenuTrigger asChild>
188
- <IconButton
189
- size="sm"
190
- variant="transparent"
191
- icon="more2Line"
192
- disabled={isSettingDefault}
193
- aria-label={`Open ${harness.label} harness actions`}
194
- />
195
- </DropdownMenuTrigger>
196
- <DropdownMenuContent align="end">
197
- <DropdownMenuItem
198
- icon="starLine"
199
- disabled={isSettingDefault || !isAvailable}
200
- onSelect={() => {
201
- onSetDefault(harness);
202
- }}
203
- >
204
- Set as default
205
- </DropdownMenuItem>
206
- </DropdownMenuContent>
207
- </DropdownMenu>
210
+ {defaultError ? (
211
+ <Callout role="alert" type="error">
212
+ <Text size="sm">{defaultError}</Text>
213
+ </Callout>
208
214
  ) : null}
209
- </div>
210
- {defaultError ? (
211
- <Callout role="alert" type="error">
212
- <Text size="sm">{defaultError}</Text>
213
- </Callout>
214
- ) : null}
215
- </li>
215
+ </li>
216
+ </PanelRow>
216
217
  );
217
218
  }
218
219
 
@@ -224,18 +225,24 @@ function harnessUnavailableCopy(isDefault: boolean): string {
224
225
 
225
226
  function HarnessRowsSkeleton() {
226
227
  return (
227
- <ul
228
- role="status"
229
- aria-label="Loading harnesses"
230
- className={cn('divide-y divide-border-neutral-base', SURFACE_CLASS)}
231
- >
232
- {[0, 1].map((row) => (
233
- <li key={row} className="flex items-center gap-cluster px-row py-row">
234
- <Skeleton className="size-16 shrink-0" />
235
- <Skeleton className="h-16 w-120" />
236
- <Skeleton className="ml-auto size-28 shrink-0" />
237
- </li>
238
- ))}
239
- </ul>
228
+ <Panel>
229
+ <PanelBody asChild>
230
+ <ul role="status" aria-label="Loading harnesses">
231
+ {[0, 1].map((row) => (
232
+ <PanelRow
233
+ asChild
234
+ className="justify-start gap-cluster hover:bg-background-neutral-base"
235
+ key={row}
236
+ >
237
+ <li>
238
+ <Skeleton className="size-16 shrink-0" />
239
+ <Skeleton className="h-16 w-120" />
240
+ <Skeleton className="ml-auto size-28 shrink-0" />
241
+ </li>
242
+ </PanelRow>
243
+ ))}
244
+ </ul>
245
+ </PanelBody>
246
+ </Panel>
240
247
  );
241
248
  }
@@ -0,0 +1,21 @@
1
+ import {Panel, PanelBody, PanelCell, PanelGrid} from '@shipfox/react-ui/panel';
2
+ import {Skeleton} from '@shipfox/react-ui/skeleton';
3
+
4
+ export function ModelProviderGridSkeleton({label}: {label: string}) {
5
+ return (
6
+ <Panel>
7
+ <PanelBody>
8
+ <PanelGrid role="status" aria-busy="true" aria-label={label}>
9
+ {[0, 1, 2, 3].map((tile) => (
10
+ <PanelCell key={tile}>
11
+ <div className="flex items-center justify-between gap-cluster px-row py-row">
12
+ <Skeleton className="h-16 w-100" />
13
+ <Skeleton className="h-16 w-64 shrink-0" />
14
+ </div>
15
+ </PanelCell>
16
+ ))}
17
+ </PanelGrid>
18
+ </PanelBody>
19
+ </Panel>
20
+ );
21
+ }
@@ -89,8 +89,8 @@ describe('WorkspaceModelProvidersSection', () => {
89
89
  expect(screen.getByText('Default provider')).toHaveClass('sr-only');
90
90
  expect(screen.getByText('Available providers')).toBeVisible();
91
91
  expect(
92
- screen.getByText('Providers that can be configured for agent steps in this workspace.'),
93
- ).toBeVisible();
92
+ screen.queryByText('Providers that can be configured for agent steps in this workspace.'),
93
+ ).not.toBeInTheDocument();
94
94
  expect(screen.getByText('OpenAI')).toBeVisible();
95
95
  const customProviderButton = screen.getByRole('button', {name: 'Configure custom provider'});
96
96
  expect(within(customProviderButton).getByText('Custom')).toBeVisible();