@shipfox/client-agent 0.2.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 (182) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +4 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +93 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/add-custom-provider-card.d.ts +4 -0
  10. package/dist/components/add-custom-provider-card.d.ts.map +1 -0
  11. package/dist/components/add-custom-provider-card.js +41 -0
  12. package/dist/components/add-custom-provider-card.js.map +1 -0
  13. package/dist/components/agent-workflow-example.d.ts +11 -0
  14. package/dist/components/agent-workflow-example.d.ts.map +1 -0
  15. package/dist/components/agent-workflow-example.js +38 -0
  16. package/dist/components/agent-workflow-example.js.map +1 -0
  17. package/dist/components/available-provider-card.d.ts +6 -0
  18. package/dist/components/available-provider-card.d.ts.map +1 -0
  19. package/dist/components/available-provider-card.js +41 -0
  20. package/dist/components/available-provider-card.js.map +1 -0
  21. package/dist/components/available-providers-grid.d.ts +10 -0
  22. package/dist/components/available-providers-grid.d.ts.map +1 -0
  23. package/dist/components/available-providers-grid.js +89 -0
  24. package/dist/components/available-providers-grid.js.map +1 -0
  25. package/dist/components/change-default-model-form.d.ts +9 -0
  26. package/dist/components/change-default-model-form.d.ts.map +1 -0
  27. package/dist/components/change-default-model-form.js +98 -0
  28. package/dist/components/change-default-model-form.js.map +1 -0
  29. package/dist/components/custom-model-provider-api-options.d.ts +7 -0
  30. package/dist/components/custom-model-provider-api-options.d.ts.map +1 -0
  31. package/dist/components/custom-model-provider-api-options.js +23 -0
  32. package/dist/components/custom-model-provider-api-options.js.map +1 -0
  33. package/dist/components/custom-model-provider-form.d.ts +8 -0
  34. package/dist/components/custom-model-provider-form.d.ts.map +1 -0
  35. package/dist/components/custom-model-provider-form.js +667 -0
  36. package/dist/components/custom-model-provider-form.js.map +1 -0
  37. package/dist/components/custom-model-provider-form.stories.d.ts +20 -0
  38. package/dist/components/custom-model-provider-form.stories.d.ts.map +1 -0
  39. package/dist/components/custom-model-provider-form.stories.js +104 -0
  40. package/dist/components/custom-model-provider-form.stories.js.map +1 -0
  41. package/dist/components/custom-model-provider-payload.d.ts +39 -0
  42. package/dist/components/custom-model-provider-payload.d.ts.map +1 -0
  43. package/dist/components/custom-model-provider-payload.js +203 -0
  44. package/dist/components/custom-model-provider-payload.js.map +1 -0
  45. package/dist/components/custom-model-provider-slug.d.ts +3 -0
  46. package/dist/components/custom-model-provider-slug.d.ts.map +1 -0
  47. package/dist/components/custom-model-provider-slug.js +14 -0
  48. package/dist/components/custom-model-provider-slug.js.map +1 -0
  49. package/dist/components/default-model-field.d.ts +16 -0
  50. package/dist/components/default-model-field.d.ts.map +1 -0
  51. package/dist/components/default-model-field.js +60 -0
  52. package/dist/components/default-model-field.js.map +1 -0
  53. package/dist/components/form-errors.d.ts +11 -0
  54. package/dist/components/form-errors.d.ts.map +1 -0
  55. package/dist/components/form-errors.js +101 -0
  56. package/dist/components/form-errors.js.map +1 -0
  57. package/dist/components/harness-availability.d.ts +8 -0
  58. package/dist/components/harness-availability.d.ts.map +1 -0
  59. package/dist/components/harness-availability.js +16 -0
  60. package/dist/components/harness-availability.js.map +1 -0
  61. package/dist/components/harnesses-section.d.ts +4 -0
  62. package/dist/components/harnesses-section.d.ts.map +1 -0
  63. package/dist/components/harnesses-section.js +249 -0
  64. package/dist/components/harnesses-section.js.map +1 -0
  65. package/dist/components/harnesses-section.stories.d.ts +25 -0
  66. package/dist/components/harnesses-section.stories.d.ts.map +1 -0
  67. package/dist/components/harnesses-section.stories.js +138 -0
  68. package/dist/components/harnesses-section.stories.js.map +1 -0
  69. package/dist/components/model-provider-usage-modal.d.ts +11 -0
  70. package/dist/components/model-provider-usage-modal.d.ts.map +1 -0
  71. package/dist/components/model-provider-usage-modal.js +349 -0
  72. package/dist/components/model-provider-usage-modal.js.map +1 -0
  73. package/dist/components/model-provider-usage-modal.stories.d.ts +21 -0
  74. package/dist/components/model-provider-usage-modal.stories.d.ts.map +1 -0
  75. package/dist/components/model-provider-usage-modal.stories.js +115 -0
  76. package/dist/components/model-provider-usage-modal.stories.js.map +1 -0
  77. package/dist/components/model-provider-usage-target.d.ts +14 -0
  78. package/dist/components/model-provider-usage-target.d.ts.map +1 -0
  79. package/dist/components/model-provider-usage-target.js +20 -0
  80. package/dist/components/model-provider-usage-target.js.map +1 -0
  81. package/dist/components/model-providers-section.d.ts +4 -0
  82. package/dist/components/model-providers-section.d.ts.map +1 -0
  83. package/dist/components/model-providers-section.js +668 -0
  84. package/dist/components/model-providers-section.js.map +1 -0
  85. package/dist/components/model-providers-section.stories.d.ts +32 -0
  86. package/dist/components/model-providers-section.stories.d.ts.map +1 -0
  87. package/dist/components/model-providers-section.stories.js +442 -0
  88. package/dist/components/model-providers-section.stories.js.map +1 -0
  89. package/dist/components/provider-search.d.ts +4 -0
  90. package/dist/components/provider-search.d.ts.map +1 -0
  91. package/dist/components/provider-search.js +13 -0
  92. package/dist/components/provider-search.js.map +1 -0
  93. package/dist/components/supported-model-provider-catalog-entry.d.ts +8 -0
  94. package/dist/components/supported-model-provider-catalog-entry.d.ts.map +1 -0
  95. package/dist/components/supported-model-provider-catalog-entry.js +9 -0
  96. package/dist/components/supported-model-provider-catalog-entry.js.map +1 -0
  97. package/dist/components/test-and-save-form.d.ts +10 -0
  98. package/dist/components/test-and-save-form.d.ts.map +1 -0
  99. package/dist/components/test-and-save-form.js +138 -0
  100. package/dist/components/test-and-save-form.js.map +1 -0
  101. package/dist/feature.d.ts +9 -0
  102. package/dist/feature.d.ts.map +1 -0
  103. package/dist/feature.js +13 -0
  104. package/dist/feature.js.map +1 -0
  105. package/dist/hooks/api/model-providers.d.ts +334 -0
  106. package/dist/hooks/api/model-providers.d.ts.map +1 -0
  107. package/dist/hooks/api/model-providers.js +190 -0
  108. package/dist/hooks/api/model-providers.js.map +1 -0
  109. package/dist/index.d.ts +9 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +10 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/pages/model-provider-onboarding-page.d.ts +6 -0
  114. package/dist/pages/model-provider-onboarding-page.d.ts.map +1 -0
  115. package/dist/pages/model-provider-onboarding-page.js +295 -0
  116. package/dist/pages/model-provider-onboarding-page.js.map +1 -0
  117. package/dist/pages/model-provider-onboarding-page.stories.d.ts +27 -0
  118. package/dist/pages/model-provider-onboarding-page.stories.d.ts.map +1 -0
  119. package/dist/pages/model-provider-onboarding-page.stories.js +368 -0
  120. package/dist/pages/model-provider-onboarding-page.stories.js.map +1 -0
  121. package/dist/routes/model-provider.d.ts +5 -0
  122. package/dist/routes/model-provider.d.ts.map +1 -0
  123. package/dist/routes/model-provider.js +28 -0
  124. package/dist/routes/model-provider.js.map +1 -0
  125. package/dist/state/model-provider-onboarding.d.ts +3 -0
  126. package/dist/state/model-provider-onboarding.d.ts.map +1 -0
  127. package/dist/state/model-provider-onboarding.js +28 -0
  128. package/dist/state/model-provider-onboarding.js.map +1 -0
  129. package/dist/tsconfig.test.tsbuildinfo +1 -0
  130. package/package.json +114 -0
  131. package/src/components/add-custom-provider-card.tsx +32 -0
  132. package/src/components/agent-workflow-example.test.ts +74 -0
  133. package/src/components/agent-workflow-example.ts +55 -0
  134. package/src/components/available-provider-card.tsx +39 -0
  135. package/src/components/available-providers-grid.test.tsx +55 -0
  136. package/src/components/available-providers-grid.tsx +106 -0
  137. package/src/components/change-default-model-form.tsx +104 -0
  138. package/src/components/custom-model-provider-api-options.test.ts +18 -0
  139. package/src/components/custom-model-provider-api-options.ts +12 -0
  140. package/src/components/custom-model-provider-form.stories.tsx +91 -0
  141. package/src/components/custom-model-provider-form.tsx +710 -0
  142. package/src/components/custom-model-provider-payload.test.ts +170 -0
  143. package/src/components/custom-model-provider-payload.ts +264 -0
  144. package/src/components/custom-model-provider-slug.test.ts +40 -0
  145. package/src/components/custom-model-provider-slug.ts +23 -0
  146. package/src/components/default-model-field.tsx +85 -0
  147. package/src/components/form-errors.test.ts +159 -0
  148. package/src/components/form-errors.ts +102 -0
  149. package/src/components/harness-availability.test.ts +45 -0
  150. package/src/components/harness-availability.ts +35 -0
  151. package/src/components/harnesses-section.stories.tsx +129 -0
  152. package/src/components/harnesses-section.test.tsx +188 -0
  153. package/src/components/harnesses-section.tsx +245 -0
  154. package/src/components/model-provider-usage-modal.stories.tsx +106 -0
  155. package/src/components/model-provider-usage-modal.test.tsx +210 -0
  156. package/src/components/model-provider-usage-modal.tsx +326 -0
  157. package/src/components/model-provider-usage-target.ts +36 -0
  158. package/src/components/model-providers-section.stories.tsx +345 -0
  159. package/src/components/model-providers-section.test.tsx +831 -0
  160. package/src/components/model-providers-section.tsx +645 -0
  161. package/src/components/provider-search.test.ts +45 -0
  162. package/src/components/provider-search.ts +18 -0
  163. package/src/components/supported-model-provider-catalog-entry.ts +19 -0
  164. package/src/components/test-and-save-form.tsx +166 -0
  165. package/src/feature.ts +12 -0
  166. package/src/hooks/api/model-providers.test.ts +167 -0
  167. package/src/hooks/api/model-providers.ts +281 -0
  168. package/src/index.ts +8 -0
  169. package/src/pages/model-provider-onboarding-page.stories.tsx +284 -0
  170. package/src/pages/model-provider-onboarding-page.test.tsx +368 -0
  171. package/src/pages/model-provider-onboarding-page.tsx +290 -0
  172. package/src/routes/model-provider.tsx +20 -0
  173. package/src/state/model-provider-onboarding.test.ts +55 -0
  174. package/src/state/model-provider-onboarding.ts +34 -0
  175. package/test/fixtures/model-providers.ts +111 -0
  176. package/test/setup.ts +3 -0
  177. package/tsconfig.build.json +9 -0
  178. package/tsconfig.build.tsbuildinfo +1 -0
  179. package/tsconfig.json +3 -0
  180. package/tsconfig.test.json +8 -0
  181. package/vercel.json +8 -0
  182. package/vitest.config.ts +72 -0
@@ -0,0 +1,667 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Badge } from '@shipfox/react-ui/badge';
3
+ import { Button, IconButton } from '@shipfox/react-ui/button';
4
+ import { Callout } from '@shipfox/react-ui/callout';
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@shipfox/react-ui/collapsible';
6
+ import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
7
+ import { ModalBody, ModalFooter } from '@shipfox/react-ui/modal';
8
+ import { Switch } from '@shipfox/react-ui/switch';
9
+ import { Text } from '@shipfox/react-ui/typography';
10
+ import { useForm } from '@tanstack/react-form';
11
+ import { useEffect, useRef, useState } from 'react';
12
+ import { useCreateCustomModelProviderMutation, useDiscoverCustomModelProviderModelsBySlugMutation, useDiscoverCustomModelProviderModelsMutation, useUpdateCustomModelProviderMutation } from '#hooks/api/model-providers.js';
13
+ import { MODEL_PROVIDER_API_OPTIONS } from './custom-model-provider-api-options.js';
14
+ import { buildCreateCustomModelProviderBody, buildDiscoverModelsBody, buildDiscoverModelsBySlugBody, buildUpdateCustomModelProviderBody, createCustomModelProviderFormValues, createFormRowId, editCustomModelProviderFormValues } from './custom-model-provider-payload.js';
15
+ import { customModelProviderSlugError, deriveCustomModelProviderSlug } from './custom-model-provider-slug.js';
16
+ import { FormFieldSelect } from './default-model-field.js';
17
+ import { modelProviderConfigErrorField } from './form-errors.js';
18
+ export const CUSTOM_MODEL_PROVIDER_FORM_ID = 'custom-model-provider-form';
19
+ export function CustomModelProviderForm({ workspaceId, existingConfig, onSaved }) {
20
+ const createMutation = useCreateCustomModelProviderMutation();
21
+ const updateMutation = useUpdateCustomModelProviderMutation();
22
+ const discoverMutation = useDiscoverCustomModelProviderModelsMutation();
23
+ const discoverBySlugMutation = useDiscoverCustomModelProviderModelsBySlugMutation();
24
+ const [slugEdited, setSlugEdited] = useState(existingConfig !== undefined);
25
+ const [formError, setFormError] = useState();
26
+ const [discoveryStatus, setDiscoveryStatus] = useState();
27
+ const [removedDefaultNotice, setRemovedDefaultNotice] = useState(false);
28
+ const mountedRef = useRef(true);
29
+ const form = useForm({
30
+ defaultValues: existingConfig ? editCustomModelProviderFormValues(existingConfig) : createCustomModelProviderFormValues(),
31
+ onSubmit: async ({ value })=>{
32
+ setFormError(undefined);
33
+ try {
34
+ if (existingConfig) {
35
+ await updateMutation.mutateAsync({
36
+ workspaceId,
37
+ providerId: existingConfig.provider_id,
38
+ body: buildUpdateCustomModelProviderBody(existingConfig, value)
39
+ });
40
+ } else {
41
+ await createMutation.mutateAsync({
42
+ workspaceId,
43
+ body: buildCreateCustomModelProviderBody(value)
44
+ });
45
+ }
46
+ onSaved();
47
+ } catch (error) {
48
+ applyServerError(error);
49
+ }
50
+ }
51
+ });
52
+ const isPending = createMutation.isPending || updateMutation.isPending || discoverMutation.isPending || discoverBySlugMutation.isPending;
53
+ useEffect(()=>{
54
+ return ()=>{
55
+ mountedRef.current = false;
56
+ };
57
+ }, []);
58
+ async function handleDiscoverModels(values) {
59
+ setDiscoveryStatus(undefined);
60
+ try {
61
+ const response = existingConfig ? await discoverBySlugMutation.mutateAsync({
62
+ workspaceId,
63
+ providerId: existingConfig.provider_id,
64
+ body: buildDiscoverModelsBySlugBody(existingConfig, values)
65
+ }) : await discoverMutation.mutateAsync({
66
+ workspaceId,
67
+ body: buildDiscoverModelsBody(values)
68
+ });
69
+ if (!mountedRef.current) return;
70
+ const existingIds = new Set(values.models.map((model)=>model.id.trim()).filter(Boolean));
71
+ const discovered = response.models.filter((model)=>!existingIds.has(model.id));
72
+ form.setFieldValue('models', [
73
+ ...values.models,
74
+ ...discovered.map((model)=>({
75
+ client_id: createFormRowId(),
76
+ id: model.id,
77
+ label: model.label,
78
+ context_window: '',
79
+ max_output_tokens: '',
80
+ input_image: false,
81
+ reasoning: false
82
+ }))
83
+ ]);
84
+ setDiscoveryStatus(discovered.length > 0 ? `Discovered ${discovered.length} models.` : 'No models found - that is normal for some servers. Add models manually.');
85
+ } catch (error) {
86
+ if (!mountedRef.current) return;
87
+ applyServerError(error);
88
+ }
89
+ }
90
+ function applyServerError(error) {
91
+ const mapped = modelProviderConfigErrorField(error);
92
+ if (mapped.kind === 'field') {
93
+ form.setFieldMeta(mapped.field, (prev)=>({
94
+ ...prev,
95
+ errorMap: {
96
+ ...prev.errorMap,
97
+ onServer: mapped.message
98
+ }
99
+ }));
100
+ return;
101
+ }
102
+ setFormError(mapped.message);
103
+ }
104
+ return /*#__PURE__*/ _jsxs(_Fragment, {
105
+ children: [
106
+ /*#__PURE__*/ _jsxs(ModalBody, {
107
+ className: "min-h-0 flex-1 gap-0 overflow-y-auto overflow-x-clip scrollbar",
108
+ children: [
109
+ /*#__PURE__*/ _jsx("form", {
110
+ id: CUSTOM_MODEL_PROVIDER_FORM_ID,
111
+ className: "flex w-full flex-col gap-24",
112
+ noValidate: true,
113
+ onSubmit: (event)=>{
114
+ event.preventDefault();
115
+ event.stopPropagation();
116
+ void form.handleSubmit();
117
+ },
118
+ children: /*#__PURE__*/ _jsx(form.Subscribe, {
119
+ selector: (state)=>state.values,
120
+ children: (values)=>/*#__PURE__*/ _jsxs(_Fragment, {
121
+ children: [
122
+ /*#__PURE__*/ _jsxs(FormGroup, {
123
+ title: "Provider",
124
+ children: [
125
+ /*#__PURE__*/ _jsxs("div", {
126
+ className: "grid grid-cols-2 gap-12 max-[640px]:grid-cols-1",
127
+ children: [
128
+ /*#__PURE__*/ _jsx(form.Field, {
129
+ name: "display_name",
130
+ validators: {
131
+ onBlur: ({ value })=>value.trim() ? undefined : 'Display name is required.',
132
+ onSubmit: ({ value })=>value.trim() ? undefined : 'Display name is required.'
133
+ },
134
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
135
+ label: "Display name",
136
+ id: "custom-provider-display-name",
137
+ error: fieldError(field),
138
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
139
+ value: field.state.value,
140
+ onChange: (event)=>{
141
+ const displayName = event.target.value;
142
+ field.handleChange(displayName);
143
+ if (!slugEdited && !existingConfig) {
144
+ form.setFieldValue('slug', deriveCustomModelProviderSlug(displayName));
145
+ }
146
+ },
147
+ onBlur: field.handleBlur
148
+ })
149
+ })
150
+ }),
151
+ /*#__PURE__*/ _jsx(form.Field, {
152
+ name: "slug",
153
+ validators: {
154
+ onBlur: ({ value })=>existingConfig ? undefined : customModelProviderSlugError(value),
155
+ onSubmit: ({ value })=>existingConfig ? undefined : customModelProviderSlugError(value)
156
+ },
157
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
158
+ label: "Provider ID",
159
+ id: "custom-provider-slug",
160
+ error: fieldError(field),
161
+ description: "Use this ID in workflow files.",
162
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
163
+ className: "font-code",
164
+ value: field.state.value,
165
+ disabled: existingConfig !== undefined,
166
+ onChange: (event)=>{
167
+ setSlugEdited(event.target.value.trim() !== '');
168
+ field.handleChange(event.target.value);
169
+ },
170
+ onBlur: field.handleBlur
171
+ })
172
+ })
173
+ })
174
+ ]
175
+ }),
176
+ /*#__PURE__*/ _jsxs("div", {
177
+ className: "grid grid-cols-2 gap-12 max-[640px]:grid-cols-1",
178
+ children: [
179
+ /*#__PURE__*/ _jsx(form.Field, {
180
+ name: "api",
181
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
182
+ label: "Protocol",
183
+ id: "custom-provider-api",
184
+ children: /*#__PURE__*/ _jsx(FormFieldSelect, {
185
+ value: field.state.value,
186
+ onChange: (event)=>field.handleChange(event.target.value),
187
+ onBlur: field.handleBlur,
188
+ children: MODEL_PROVIDER_API_OPTIONS.map((option)=>/*#__PURE__*/ _jsx("option", {
189
+ value: option.value,
190
+ children: option.label
191
+ }, option.value))
192
+ })
193
+ })
194
+ }),
195
+ /*#__PURE__*/ _jsx(form.Field, {
196
+ name: "base_url",
197
+ validators: {
198
+ onBlur: ({ value })=>canParseUrl(value) ? undefined : 'Enter a full URL, like https://api.openai.com/v1',
199
+ onSubmit: ({ value })=>canParseUrl(value) ? undefined : 'Enter a full URL, like https://api.openai.com/v1'
200
+ },
201
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
202
+ label: "Base URL",
203
+ id: "custom-provider-base-url",
204
+ error: fieldError(field),
205
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
206
+ className: "font-code",
207
+ placeholder: "https://api.openai.com/v1",
208
+ value: field.state.value,
209
+ onChange: (event)=>field.handleChange(event.target.value),
210
+ onBlur: field.handleBlur
211
+ })
212
+ })
213
+ })
214
+ ]
215
+ }),
216
+ /*#__PURE__*/ _jsx(form.Field, {
217
+ name: "api_key",
218
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
219
+ label: "API key",
220
+ id: "custom-provider-api-key",
221
+ description: existingConfig ? 'Leave blank to keep the current key.' : 'Leave blank when the endpoint does not require a key.',
222
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
223
+ type: "password",
224
+ autoComplete: "off",
225
+ value: field.state.value,
226
+ onChange: (event)=>field.handleChange(event.target.value),
227
+ onBlur: field.handleBlur
228
+ })
229
+ })
230
+ })
231
+ ]
232
+ }),
233
+ /*#__PURE__*/ _jsx(FormGroup, {
234
+ title: "Headers",
235
+ children: /*#__PURE__*/ _jsx(form.Field, {
236
+ name: "headers",
237
+ children: (field)=>/*#__PURE__*/ _jsx(HeaderRows, {
238
+ rows: field.state.value,
239
+ onChange: (headers)=>field.handleChange(headers)
240
+ })
241
+ })
242
+ }),
243
+ /*#__PURE__*/ _jsxs(FormGroup, {
244
+ title: "Models",
245
+ children: [
246
+ /*#__PURE__*/ _jsxs("div", {
247
+ className: "flex flex-wrap items-center justify-between gap-8",
248
+ children: [
249
+ /*#__PURE__*/ _jsx(Button, {
250
+ type: "button",
251
+ size: "sm",
252
+ variant: "secondary",
253
+ iconLeft: "downloadLine",
254
+ disabled: !canParseUrl(values.base_url),
255
+ isLoading: discoverMutation.isPending || discoverBySlugMutation.isPending,
256
+ onClick: ()=>{
257
+ void handleDiscoverModels(values);
258
+ },
259
+ children: "Fetch models"
260
+ }),
261
+ discoveryStatus ? /*#__PURE__*/ _jsx(Text, {
262
+ size: "sm",
263
+ className: "text-foreground-neutral-muted",
264
+ role: "status",
265
+ children: discoveryStatus
266
+ }) : null
267
+ ]
268
+ }),
269
+ /*#__PURE__*/ _jsx(form.Field, {
270
+ name: "models",
271
+ validators: {
272
+ onSubmit: ({ value })=>value.some((model)=>model.id.trim() && model.label.trim()) ? undefined : 'Add at least one model.'
273
+ },
274
+ children: (field)=>/*#__PURE__*/ _jsxs(_Fragment, {
275
+ children: [
276
+ /*#__PURE__*/ _jsx(ModelRows, {
277
+ rows: field.state.value,
278
+ defaultModel: values.default_model,
279
+ onDefaultRemoved: ()=>setRemovedDefaultNotice(true),
280
+ onChange: (models)=>field.handleChange(models),
281
+ onDefaultChange: (defaultModel)=>{
282
+ setRemovedDefaultNotice(false);
283
+ form.setFieldValue('default_model', defaultModel);
284
+ }
285
+ }),
286
+ fieldError(field) ? /*#__PURE__*/ _jsx(Text, {
287
+ size: "sm",
288
+ className: "text-foreground-highlight-error",
289
+ children: fieldError(field)
290
+ }) : null
291
+ ]
292
+ })
293
+ }),
294
+ removedDefaultNotice ? /*#__PURE__*/ _jsx(Callout, {
295
+ role: "alert",
296
+ type: "warning",
297
+ children: /*#__PURE__*/ _jsx(Text, {
298
+ size: "sm",
299
+ children: "Default model removed - choose a new default or keep none."
300
+ })
301
+ }) : null,
302
+ /*#__PURE__*/ _jsx(form.Field, {
303
+ name: "default_model",
304
+ validators: {
305
+ onBlur: ({ value })=>validateDefaultModel(value, values.models),
306
+ onSubmit: ({ value })=>validateDefaultModel(value, values.models)
307
+ },
308
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
309
+ label: "Default model",
310
+ id: "custom-provider-default-model",
311
+ error: fieldError(field),
312
+ children: /*#__PURE__*/ _jsxs(FormFieldSelect, {
313
+ value: field.state.value,
314
+ onChange: (event)=>{
315
+ setRemovedDefaultNotice(false);
316
+ field.handleChange(event.target.value);
317
+ },
318
+ onBlur: field.handleBlur,
319
+ children: [
320
+ /*#__PURE__*/ _jsx("option", {
321
+ value: "",
322
+ children: "No default"
323
+ }),
324
+ values.models.filter((model)=>model.id.trim()).map((model)=>/*#__PURE__*/ _jsx("option", {
325
+ value: model.id,
326
+ children: model.label.trim() || model.id
327
+ }, model.client_id))
328
+ ]
329
+ })
330
+ })
331
+ })
332
+ ]
333
+ })
334
+ ]
335
+ })
336
+ })
337
+ }),
338
+ formError ? /*#__PURE__*/ _jsx(Callout, {
339
+ role: "alert",
340
+ type: "error",
341
+ className: "mt-16",
342
+ children: /*#__PURE__*/ _jsxs("div", {
343
+ className: "flex flex-col gap-8",
344
+ children: [
345
+ /*#__PURE__*/ _jsx(Text, {
346
+ size: "sm",
347
+ bold: true,
348
+ children: "Could not save provider"
349
+ }),
350
+ /*#__PURE__*/ _jsx(Text, {
351
+ size: "sm",
352
+ children: formError
353
+ })
354
+ ]
355
+ })
356
+ }) : null
357
+ ]
358
+ }),
359
+ /*#__PURE__*/ _jsxs(ModalFooter, {
360
+ children: [
361
+ /*#__PURE__*/ _jsx(Text, {
362
+ size: "sm",
363
+ className: "mr-auto text-foreground-neutral-muted",
364
+ children: "Saving runs a live test against the endpoint."
365
+ }),
366
+ /*#__PURE__*/ _jsx(Button, {
367
+ type: "submit",
368
+ form: CUSTOM_MODEL_PROVIDER_FORM_ID,
369
+ isLoading: isPending,
370
+ children: "Test & save"
371
+ })
372
+ ]
373
+ })
374
+ ]
375
+ });
376
+ }
377
+ function FormGroup({ title, children }) {
378
+ return /*#__PURE__*/ _jsxs("section", {
379
+ className: "flex flex-col gap-16 border-t border-border-neutral-base pt-16 first:border-t-0 first:pt-0",
380
+ children: [
381
+ /*#__PURE__*/ _jsx(Text, {
382
+ size: "xs",
383
+ className: "font-medium uppercase text-foreground-neutral-muted",
384
+ children: title
385
+ }),
386
+ children
387
+ ]
388
+ });
389
+ }
390
+ function HeaderRows({ rows, onChange }) {
391
+ const addButtonRef = useRef(null);
392
+ return /*#__PURE__*/ _jsxs("div", {
393
+ className: "flex flex-col gap-10",
394
+ children: [
395
+ rows.map((row, index)=>/*#__PURE__*/ _jsxs("div", {
396
+ className: "grid grid-cols-[1fr_1fr_auto_auto] items-end gap-x-8 gap-y-4 max-[760px]:grid-cols-1",
397
+ children: [
398
+ /*#__PURE__*/ _jsx(FormField, {
399
+ label: "Name",
400
+ id: `custom-header-name-${row.client_id}`,
401
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
402
+ className: "font-code",
403
+ value: row.name,
404
+ onChange: (event)=>updateRow(rows, index, {
405
+ ...row,
406
+ name: event.target.value
407
+ }, onChange)
408
+ })
409
+ }),
410
+ /*#__PURE__*/ _jsx(FormField, {
411
+ label: "Value",
412
+ id: `custom-header-value-${row.client_id}`,
413
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
414
+ type: row.secret ? 'password' : 'text',
415
+ placeholder: row.hasStoredValue ? 'Unchanged' : undefined,
416
+ value: row.value,
417
+ onChange: (event)=>updateRow(rows, index, {
418
+ ...row,
419
+ value: event.target.value
420
+ }, onChange)
421
+ })
422
+ }),
423
+ /*#__PURE__*/ _jsxs("div", {
424
+ className: "flex h-32 items-center gap-8 text-sm text-foreground-neutral-muted",
425
+ children: [
426
+ /*#__PURE__*/ _jsx(Switch, {
427
+ size: "sm",
428
+ "aria-label": `Mark header ${index + 1} as secret`,
429
+ checked: row.secret,
430
+ onCheckedChange: (checked)=>updateRow(rows, index, {
431
+ ...row,
432
+ secret: checked
433
+ }, onChange)
434
+ }),
435
+ "Secret"
436
+ ]
437
+ }),
438
+ /*#__PURE__*/ _jsx(IconButton, {
439
+ type: "button",
440
+ size: "sm",
441
+ variant: "transparent",
442
+ icon: "deleteBinLine",
443
+ "aria-label": "Remove header",
444
+ onClick: ()=>{
445
+ onChange(rows.filter((_, rowIndex)=>rowIndex !== index));
446
+ focusAfterRender(addButtonRef);
447
+ }
448
+ })
449
+ ]
450
+ }, row.client_id)),
451
+ /*#__PURE__*/ _jsx(Button, {
452
+ ref: addButtonRef,
453
+ type: "button",
454
+ size: "sm",
455
+ variant: "secondary",
456
+ iconLeft: "addLine",
457
+ onClick: ()=>{
458
+ const clientId = createFormRowId();
459
+ onChange([
460
+ ...rows,
461
+ {
462
+ client_id: clientId,
463
+ name: '',
464
+ value: '',
465
+ secret: false,
466
+ hasStoredValue: false,
467
+ storedName: ''
468
+ }
469
+ ]);
470
+ focusAfterRender(`custom-header-name-${clientId}`);
471
+ },
472
+ children: "Add header"
473
+ })
474
+ ]
475
+ });
476
+ }
477
+ function ModelRows({ rows, defaultModel, onChange, onDefaultChange, onDefaultRemoved }) {
478
+ const addButtonRef = useRef(null);
479
+ return /*#__PURE__*/ _jsxs("div", {
480
+ className: "flex flex-col gap-10",
481
+ children: [
482
+ rows.map((row, index)=>/*#__PURE__*/ _jsxs("div", {
483
+ className: "rounded-8 border border-border-neutral-base p-12",
484
+ children: [
485
+ /*#__PURE__*/ _jsxs("div", {
486
+ className: "grid grid-cols-[1fr_1fr_auto] items-end gap-8 max-[760px]:grid-cols-1",
487
+ children: [
488
+ /*#__PURE__*/ _jsx(FormField, {
489
+ label: "Model id",
490
+ id: `custom-model-id-${row.client_id}`,
491
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
492
+ className: "font-code",
493
+ value: row.id,
494
+ onChange: (event)=>updateRow(rows, index, {
495
+ ...row,
496
+ id: event.target.value
497
+ }, onChange)
498
+ })
499
+ }),
500
+ /*#__PURE__*/ _jsx(FormField, {
501
+ label: "Label",
502
+ id: `custom-model-label-${row.client_id}`,
503
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
504
+ value: row.label,
505
+ onChange: (event)=>updateRow(rows, index, {
506
+ ...row,
507
+ label: event.target.value
508
+ }, onChange)
509
+ })
510
+ }),
511
+ /*#__PURE__*/ _jsxs("div", {
512
+ className: "flex items-center gap-8",
513
+ children: [
514
+ defaultModel && row.id === defaultModel ? /*#__PURE__*/ _jsx(Badge, {
515
+ variant: "neutral",
516
+ children: "Default"
517
+ }) : null,
518
+ /*#__PURE__*/ _jsx(IconButton, {
519
+ type: "button",
520
+ size: "sm",
521
+ variant: "transparent",
522
+ icon: "deleteBinLine",
523
+ "aria-label": "Remove model",
524
+ onClick: ()=>{
525
+ const nextRows = rows.filter((_, rowIndex)=>rowIndex !== index);
526
+ onChange(nextRows);
527
+ focusAfterRender(addButtonRef);
528
+ if (row.id === defaultModel) {
529
+ onDefaultChange('');
530
+ onDefaultRemoved();
531
+ }
532
+ }
533
+ })
534
+ ]
535
+ })
536
+ ]
537
+ }),
538
+ /*#__PURE__*/ _jsxs(Collapsible, {
539
+ children: [
540
+ /*#__PURE__*/ _jsx(CollapsibleTrigger, {
541
+ asChild: true,
542
+ children: /*#__PURE__*/ _jsx(Button, {
543
+ type: "button",
544
+ size: "sm",
545
+ variant: "transparentMuted",
546
+ className: "mt-8",
547
+ children: "Defaults: 128k context, 16k output, text-only"
548
+ })
549
+ }),
550
+ /*#__PURE__*/ _jsx(CollapsibleContent, {
551
+ children: /*#__PURE__*/ _jsxs("div", {
552
+ className: "mt-8 grid grid-cols-2 gap-8 max-[640px]:grid-cols-1",
553
+ children: [
554
+ /*#__PURE__*/ _jsx(FormField, {
555
+ label: "Context window",
556
+ id: `custom-model-context-${row.client_id}`,
557
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
558
+ type: "number",
559
+ inputMode: "numeric",
560
+ placeholder: "128000",
561
+ value: row.context_window,
562
+ onChange: (event)=>updateRow(rows, index, {
563
+ ...row,
564
+ context_window: event.target.value
565
+ }, onChange)
566
+ })
567
+ }),
568
+ /*#__PURE__*/ _jsx(FormField, {
569
+ label: "Max output tokens",
570
+ id: `custom-model-output-${row.client_id}`,
571
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
572
+ type: "number",
573
+ inputMode: "numeric",
574
+ placeholder: "16384",
575
+ value: row.max_output_tokens,
576
+ onChange: (event)=>updateRow(rows, index, {
577
+ ...row,
578
+ max_output_tokens: event.target.value
579
+ }, onChange)
580
+ })
581
+ }),
582
+ /*#__PURE__*/ _jsxs("div", {
583
+ className: "flex h-32 items-center gap-8 text-sm text-foreground-neutral-muted",
584
+ children: [
585
+ /*#__PURE__*/ _jsx(Switch, {
586
+ size: "sm",
587
+ "aria-label": `Enable image input for model ${index + 1}`,
588
+ checked: row.input_image,
589
+ onCheckedChange: (checked)=>updateRow(rows, index, {
590
+ ...row,
591
+ input_image: checked
592
+ }, onChange)
593
+ }),
594
+ "Image input"
595
+ ]
596
+ }),
597
+ /*#__PURE__*/ _jsxs("div", {
598
+ className: "flex h-32 items-center gap-8 text-sm text-foreground-neutral-muted",
599
+ children: [
600
+ /*#__PURE__*/ _jsx(Switch, {
601
+ size: "sm",
602
+ "aria-label": `Enable reasoning for model ${index + 1}`,
603
+ checked: row.reasoning,
604
+ onCheckedChange: (checked)=>updateRow(rows, index, {
605
+ ...row,
606
+ reasoning: checked
607
+ }, onChange)
608
+ }),
609
+ "Reasoning"
610
+ ]
611
+ })
612
+ ]
613
+ })
614
+ })
615
+ ]
616
+ })
617
+ ]
618
+ }, row.client_id)),
619
+ /*#__PURE__*/ _jsx(Button, {
620
+ ref: addButtonRef,
621
+ type: "button",
622
+ size: "sm",
623
+ variant: "secondary",
624
+ iconLeft: "addLine",
625
+ onClick: ()=>{
626
+ const clientId = createFormRowId();
627
+ onChange([
628
+ ...rows,
629
+ {
630
+ client_id: clientId,
631
+ id: '',
632
+ label: '',
633
+ context_window: '',
634
+ max_output_tokens: '',
635
+ input_image: false,
636
+ reasoning: false
637
+ }
638
+ ]);
639
+ focusAfterRender(`custom-model-id-${clientId}`);
640
+ },
641
+ children: "Add model"
642
+ })
643
+ ]
644
+ });
645
+ }
646
+ function updateRow(rows, index, row, onChange) {
647
+ onChange(rows.map((candidate, rowIndex)=>rowIndex === index ? row : candidate));
648
+ }
649
+ function focusAfterRender(target) {
650
+ requestAnimationFrame(()=>{
651
+ const element = typeof target === 'string' ? document.getElementById(target) : target.current;
652
+ element?.focus();
653
+ });
654
+ }
655
+ function validateDefaultModel(value, models) {
656
+ return value.trim() && !models.some((model)=>model.id.trim() === value.trim()) ? 'Choose a model from the models list.' : undefined;
657
+ }
658
+ function canParseUrl(value) {
659
+ try {
660
+ new URL(value);
661
+ return true;
662
+ } catch {
663
+ return false;
664
+ }
665
+ }
666
+
667
+ //# sourceMappingURL=custom-model-provider-form.js.map