@shipfox/client-agent 23.0.0 → 25.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 (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/dist/components/agent-workflow-example.d.ts.map +1 -1
  4. package/dist/components/agent-workflow-example.js +0 -1
  5. package/dist/components/agent-workflow-example.js.map +1 -1
  6. package/dist/components/model-providers-section.stories.js +2 -1
  7. package/dist/components/model-providers-section.stories.js.map +1 -1
  8. package/dist/core/models.d.ts +2 -0
  9. package/dist/core/models.d.ts.map +1 -1
  10. package/dist/core/models.js.map +1 -1
  11. package/dist/core/provider-policy.d.ts.map +1 -1
  12. package/dist/core/provider-policy.js +8 -2
  13. package/dist/core/provider-policy.js.map +1 -1
  14. package/dist/hooks/api/model-provider-mapper.d.ts.map +1 -1
  15. package/dist/hooks/api/model-provider-mapper.js +3 -1
  16. package/dist/hooks/api/model-provider-mapper.js.map +1 -1
  17. package/dist/tsconfig.test.tsbuildinfo +1 -1
  18. package/package.json +4 -4
  19. package/src/components/agent-workflow-example.test.ts +2 -2
  20. package/src/components/agent-workflow-example.ts +0 -1
  21. package/src/components/harness-availability.test.ts +4 -1
  22. package/src/components/model-providers-section.stories.tsx +3 -1
  23. package/src/components/model-providers-section.test.tsx +28 -1
  24. package/src/core/models.ts +2 -0
  25. package/src/core/provider-policy.test.ts +78 -0
  26. package/src/core/provider-policy.ts +8 -2
  27. package/src/hooks/api/model-provider-mapper.test.ts +19 -1
  28. package/src/hooks/api/model-provider-mapper.ts +2 -0
  29. package/src/pages/model-provider-onboarding-page.test.tsx +7 -5
  30. package/test/fixtures/model-providers.ts +3 -0
  31. 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": "23.0.0",
4
+ "version": "25.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -28,10 +28,10 @@
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "@tanstack/react-form": "^1.32.0",
30
30
  "@tanstack/react-query": "^5.101.0",
31
- "@shipfox/api-agent-dto": "14.0.0",
32
31
  "@shipfox/client-api": "6.0.1",
33
- "@shipfox/client-shell": "23.0.0",
34
- "@shipfox/client-ui": "22.0.3",
32
+ "@shipfox/client-shell": "25.0.0",
33
+ "@shipfox/client-ui": "24.0.0",
34
+ "@shipfox/api-agent-dto": "16.0.0",
35
35
  "@shipfox/react-ui": "2.2.0"
36
36
  },
37
37
  "peerDependencies": {
@@ -68,7 +68,7 @@ describe('buildAgentWorkflowExample', () => {
68
68
  expect(lines).toContain(expectedHarness);
69
69
  expect(lines).toContain(expectedProvider);
70
70
  expect(lines).toContain(expectedModel);
71
- expect(example.highlightedLineRange).toEqual({startLine: 11, endLine: 13});
72
- expect(lines.slice(10, 13)).toEqual([expectedHarness, expectedProvider, expectedModel]);
71
+ expect(example.highlightedLineRange).toEqual({startLine: 10, endLine: 12});
72
+ expect(lines.slice(9, 12)).toEqual([expectedHarness, expectedProvider, expectedModel]);
73
73
  });
74
74
  });
@@ -22,7 +22,6 @@ export function buildAgentWorkflowExample({
22
22
  'triggers:',
23
23
  ' on_demand:',
24
24
  ' source: manual',
25
- ' event: fire',
26
25
  'jobs:',
27
26
  ' agent:',
28
27
  ' runner: ubuntu-latest',
@@ -44,7 +44,9 @@ describe('harness availability', () => {
44
44
 
45
45
  test('marks harnesses available from a managed-only catalog without workspace configs', () => {
46
46
  const catalog = toProviderCatalog(
47
- modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled'),
47
+ modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled', {
48
+ managedProviderId: 'shipfox',
49
+ }),
48
50
  );
49
51
 
50
52
  expect(isHarnessAvailable(getHarness('pi'), [], catalog)).toBe(true);
@@ -61,6 +63,7 @@ describe('harness availability', () => {
61
63
  }),
62
64
  ],
63
65
  'disabled',
66
+ {managedProviderId: 'shipfox'},
64
67
  ),
65
68
  );
66
69
 
@@ -239,7 +239,9 @@ function fetchForScenario(scenario: Scenario): typeof fetch {
239
239
  return Promise.resolve(
240
240
  jsonResponse({
241
241
  providers: catalogForScenario(scenario),
242
- ...(scenario === 'managed-only' ? {workspace_providers: 'disabled'} : {}),
242
+ ...(scenario === 'managed-only'
243
+ ? {workspace_providers: 'disabled', managed_provider_id: 'shipfox'}
244
+ : {}),
243
245
  }),
244
246
  );
245
247
  }
@@ -66,7 +66,11 @@ describe('WorkspaceModelProvidersSection', () => {
66
66
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
67
67
  if (requestPath(input).endsWith('/agent/model-provider-catalog')) {
68
68
  return Promise.resolve(
69
- jsonResponse(modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled')),
69
+ jsonResponse(
70
+ modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled', {
71
+ managedProviderId: 'shipfox',
72
+ }),
73
+ ),
70
74
  );
71
75
  }
72
76
  return Promise.resolve(
@@ -101,6 +105,29 @@ describe('WorkspaceModelProvidersSection', () => {
101
105
  expect(usageDialog).toHaveTextContent('model: claude-opus-4-8');
102
106
  });
103
107
 
108
+ test('renders managed provider models from an older managed-only catalog response', async () => {
109
+ const fetchImpl = vi.fn((input: RequestInfo | URL) => {
110
+ if (requestPath(input).endsWith('/agent/model-provider-catalog')) {
111
+ return Promise.resolve(
112
+ jsonResponse({
113
+ providers: [managedModelProviderEntry()],
114
+ workspace_providers: 'disabled',
115
+ }),
116
+ );
117
+ }
118
+ return Promise.resolve(
119
+ jsonResponse(modelProviderConfigsResponse({configs: [], default_provider_id: null})),
120
+ );
121
+ });
122
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
123
+
124
+ renderModelProviders(<WorkspaceModelProvidersSection workspaceId={AGENT_TEST_WORKSPACE_ID} />);
125
+
126
+ expect(await screen.findByText('Managed provider')).toBeVisible();
127
+ expect(screen.getByText('Shipfox Managed')).toBeVisible();
128
+ expect(screen.queryByText('Managed provider unavailable')).not.toBeInTheDocument();
129
+ });
130
+
104
131
  test('renders configured, available, and unsupported providers', async () => {
105
132
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
106
133
  if (requestPath(input).endsWith('/agent/model-provider-catalog')) {
@@ -79,6 +79,8 @@ export type ProviderConfig = BuiltinProviderConfig | CustomProviderConfig;
79
79
  export interface ProviderCatalog {
80
80
  readonly providers: readonly ProviderCatalogEntry[];
81
81
  readonly workspaceProviders: WorkspaceProvidersPolicy;
82
+ readonly managedProviderId: string | null;
83
+ readonly instanceDefaultProviderId: string | null;
82
84
  }
83
85
 
84
86
  export interface ProviderConfiguration {
@@ -0,0 +1,78 @@
1
+ import {toProviderCatalog} from '#hooks/api/model-provider-mapper.js';
2
+ import {
3
+ managedModelProviderEntry,
4
+ modelProviderCatalogResponse,
5
+ modelProviderEntry,
6
+ unsupportedModelProviderEntry,
7
+ } from '#test/fixtures/model-providers.js';
8
+ import {isManagedOnlyCatalog, managedProviderFromCatalog} from './provider-policy.js';
9
+
10
+ describe('managedProviderFromCatalog', () => {
11
+ test('returns the managed entry from a managed-only catalog', () => {
12
+ const catalog = toProviderCatalog(
13
+ modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled', {
14
+ managedProviderId: 'shipfox',
15
+ }),
16
+ );
17
+
18
+ expect(managedProviderFromCatalog(catalog)).toMatchObject({
19
+ id: 'shipfox',
20
+ kind: 'supported',
21
+ });
22
+ expect(isManagedOnlyCatalog(catalog)).toBe(true);
23
+ });
24
+
25
+ test('returns the managed entry from a mixed catalog under the enabled policy', () => {
26
+ const catalog = toProviderCatalog(
27
+ modelProviderCatalogResponse([modelProviderEntry(), managedModelProviderEntry()], 'enabled', {
28
+ managedProviderId: 'shipfox',
29
+ }),
30
+ );
31
+
32
+ expect(managedProviderFromCatalog(catalog)).toMatchObject({
33
+ id: 'shipfox',
34
+ kind: 'supported',
35
+ });
36
+ expect(isManagedOnlyCatalog(catalog)).toBe(false);
37
+ });
38
+
39
+ test('falls back to the supported entry for older managed-only catalogs', () => {
40
+ const catalog = toProviderCatalog(
41
+ modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled'),
42
+ );
43
+
44
+ expect(managedProviderFromCatalog(catalog)).toMatchObject({
45
+ id: 'shipfox',
46
+ kind: 'supported',
47
+ });
48
+ expect(isManagedOnlyCatalog(catalog)).toBe(true);
49
+ });
50
+
51
+ test('returns undefined without a managed provider id in a mixed catalog', () => {
52
+ const catalog = toProviderCatalog(modelProviderCatalogResponse());
53
+
54
+ expect(managedProviderFromCatalog(catalog)).toBeUndefined();
55
+ expect(managedProviderFromCatalog(undefined)).toBeUndefined();
56
+ expect(isManagedOnlyCatalog(catalog)).toBe(false);
57
+ });
58
+
59
+ test('returns undefined when the managed provider id is absent from the catalog', () => {
60
+ const catalog = toProviderCatalog(
61
+ modelProviderCatalogResponse([modelProviderEntry()], 'enabled', {
62
+ managedProviderId: 'missing-provider',
63
+ }),
64
+ );
65
+
66
+ expect(managedProviderFromCatalog(catalog)).toBeUndefined();
67
+ });
68
+
69
+ test('returns undefined when the managed provider entry is unsupported', () => {
70
+ const catalog = toProviderCatalog(
71
+ modelProviderCatalogResponse([unsupportedModelProviderEntry()], 'enabled', {
72
+ managedProviderId: 'amazon-bedrock',
73
+ }),
74
+ );
75
+
76
+ expect(managedProviderFromCatalog(catalog)).toBeUndefined();
77
+ });
78
+ });
@@ -21,8 +21,14 @@ export function isManagedOnlyCatalog(catalog: ProviderCatalog | undefined): bool
21
21
  export function managedProviderFromCatalog(
22
22
  catalog: ProviderCatalog | undefined,
23
23
  ): SupportedProvider | undefined {
24
- if (!isManagedOnlyCatalog(catalog)) return undefined;
25
- return catalog?.providers.find(isSupportedProvider);
24
+ const providers = catalog?.providers;
25
+ const managedProviderId = catalog?.managedProviderId;
26
+ if (providers === undefined) return undefined;
27
+ if (managedProviderId === null || managedProviderId === undefined) {
28
+ return isManagedOnlyCatalog(catalog) ? providers.find(isSupportedProvider) : undefined;
29
+ }
30
+ const managedEntry = providers.find((provider) => provider.id === managedProviderId);
31
+ return managedEntry !== undefined && isSupportedProvider(managedEntry) ? managedEntry : undefined;
26
32
  }
27
33
 
28
34
  export function availableProviders(
@@ -20,10 +20,14 @@ test('maps provider catalog entries before they reach the client domain', () =>
20
20
 
21
21
  test('maps the managed-only workspace policy and managed provider models', () => {
22
22
  const catalog = toProviderCatalog(
23
- modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled'),
23
+ modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled', {
24
+ managedProviderId: 'shipfox',
25
+ }),
24
26
  );
25
27
 
26
28
  expect(catalog.workspaceProviders).toBe('disabled');
29
+ expect(catalog.managedProviderId).toBe('shipfox');
30
+ expect(catalog.instanceDefaultProviderId).toBeNull();
27
31
  expect(catalog.providers[0]).toEqual(
28
32
  expect.objectContaining({
29
33
  id: 'shipfox',
@@ -35,6 +39,20 @@ test('maps the managed-only workspace policy and managed provider models', () =>
35
39
  );
36
40
  });
37
41
 
42
+ test('maps the managed provider id and instance default provider id from a mixed catalog', () => {
43
+ const catalog = toProviderCatalog(
44
+ modelProviderCatalogResponse([modelProviderEntry(), managedModelProviderEntry()], 'enabled', {
45
+ managedProviderId: 'shipfox',
46
+ instanceDefaultProviderId: 'anthropic',
47
+ }),
48
+ );
49
+
50
+ expect(catalog.workspaceProviders).toBe('enabled');
51
+ expect(catalog.managedProviderId).toBe('shipfox');
52
+ expect(catalog.instanceDefaultProviderId).toBe('anthropic');
53
+ expect(catalog.providers).toHaveLength(2);
54
+ });
55
+
38
56
  test('maps configuration response defaults and provider config fields', () => {
39
57
  const configuration = toProviderConfiguration(modelProviderConfigsResponse());
40
58
 
@@ -24,6 +24,8 @@ export function toProviderCatalog(response: ModelProviderCatalogResponseDto): Pr
24
24
  return {
25
25
  providers: response.providers.map(toProviderCatalogEntry),
26
26
  workspaceProviders: response.workspace_providers ?? 'enabled',
27
+ managedProviderId: response.managed_provider_id ?? null,
28
+ instanceDefaultProviderId: response.instance_default_provider_id ?? null,
27
29
  };
28
30
  }
29
31
 
@@ -79,11 +79,13 @@ describe('ModelProviderOnboardingPage', () => {
79
79
 
80
80
  test('continues directly when workspace providers are managed by the instance', async () => {
81
81
  const onConfigured = vi.fn();
82
- const fetchImpl = vi
83
- .fn()
84
- .mockResolvedValue(
85
- jsonResponse(modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled')),
86
- );
82
+ const fetchImpl = vi.fn().mockResolvedValue(
83
+ jsonResponse(
84
+ modelProviderCatalogResponse([managedModelProviderEntry()], 'disabled', {
85
+ managedProviderId: 'shipfox',
86
+ }),
87
+ ),
88
+ );
87
89
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
88
90
 
89
91
  renderOnboarding(
@@ -113,10 +113,13 @@ export function customModelProviderConfig(
113
113
  export function modelProviderCatalogResponse(
114
114
  modelProviders: ModelProviderCatalogEntryDto[] = [modelProviderEntry()],
115
115
  workspaceProviders?: WorkspaceProvidersPolicy,
116
+ providerIds: {managedProviderId?: string | null; instanceDefaultProviderId?: string | null} = {},
116
117
  ): ModelProviderCatalogResponseDto {
117
118
  return {
118
119
  providers: modelProviders,
119
120
  ...(workspaceProviders === undefined ? {} : {workspace_providers: workspaceProviders}),
121
+ managed_provider_id: providerIds.managedProviderId ?? null,
122
+ instance_default_provider_id: providerIds.instanceDefaultProviderId ?? null,
120
123
  };
121
124
  }
122
125