@@ -221,14 +221,8 @@ export function DialogCustomProvider(props: Props) {
221221
222222 setForm ( "saving" , true )
223223
224- const beforeProvider = globalSync . data . config . provider
225- const beforeDisabled = globalSync . data . config . disabled_providers
226-
227- const nextProvider = { ...( beforeProvider ?? { } ) , [ result . providerID ] : result . config }
228- const nextDisabled = ( beforeDisabled ?? [ ] ) . filter ( ( id ) => id !== result . providerID )
229-
230- globalSync . set ( "config" , "provider" , nextProvider )
231- globalSync . set ( "config" , "disabled_providers" , nextDisabled )
224+ const disabledProviders = globalSync . data . config . disabled_providers ?? [ ]
225+ const nextDisabled = disabledProviders . filter ( ( id ) => id !== result . providerID )
232226
233227 globalSync
234228 . updateConfig ( { provider : { [ result . providerID ] : result . config } , disabled_providers : nextDisabled } )
@@ -242,8 +236,6 @@ export function DialogCustomProvider(props: Props) {
242236 } )
243237 } )
244238 . catch ( ( err : unknown ) => {
245- globalSync . set ( "config" , "provider" , beforeProvider )
246- globalSync . set ( "config" , "disabled_providers" , beforeDisabled )
247239 const message = err instanceof Error ? err . message : String ( err )
248240 showToast ( { title : language . t ( "common.requestFailed" ) , description : message } )
249241 } )
@@ -265,153 +257,149 @@ export function DialogCustomProvider(props: Props) {
265257 }
266258 transition
267259 >
268- < div class = "flex flex-col gap-6 px-2.5 pb-3" >
260+ < div class = "flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh] " >
269261 < div class = "px-2.5 flex gap-4 items-center" >
270262 < ProviderIcon id = "synthetic" class = "size-5 shrink-0 icon-strong-base" />
271263 < div class = "text-16-medium text-text-strong" > Custom provider</ div >
272264 </ div >
273265
274- < div class = "px-2.5 pb-10 flex flex-col gap-6" >
275- < div class = "text-14-regular text-text-base" >
276- Configure an OpenAI-compatible provider. Fields map to the
266+ < form onSubmit = { save } class = "px-2.5 pb-6 flex flex-col gap-6" >
267+ < p class = "text-14-regular text-text-base" >
268+ Configure an OpenAI-compatible provider. See the{ " " }
277269 < Link href = "https://opencode.ai/docs/providers/#custom-provider" tabIndex = { - 1 } >
278270 provider config docs
279271 </ Link >
280272 .
273+ </ p >
274+
275+ < div class = "flex flex-col gap-4" >
276+ < TextField
277+ autofocus
278+ label = "Provider ID"
279+ placeholder = "myprovider"
280+ description = "Lowercase letters, numbers, hyphens, or underscores"
281+ value = { form . providerID }
282+ onChange = { setForm . bind ( null , "providerID" ) }
283+ validationState = { errors . providerID ? "invalid" : undefined }
284+ error = { errors . providerID }
285+ />
286+ < TextField
287+ label = "Display name"
288+ placeholder = "My AI Provider"
289+ value = { form . name }
290+ onChange = { setForm . bind ( null , "name" ) }
291+ validationState = { errors . name ? "invalid" : undefined }
292+ error = { errors . name }
293+ />
294+ < TextField
295+ label = "Base URL"
296+ placeholder = "https://api.myprovider.com/v1"
297+ value = { form . baseURL }
298+ onChange = { setForm . bind ( null , "baseURL" ) }
299+ validationState = { errors . baseURL ? "invalid" : undefined }
300+ error = { errors . baseURL }
301+ />
302+ < TextField
303+ label = "API key"
304+ placeholder = "{env:MYPROVIDER_API_KEY}"
305+ description = "Optional. Leave empty if you manage auth via headers."
306+ value = { form . apiKey }
307+ onChange = { setForm . bind ( null , "apiKey" ) }
308+ />
281309 </ div >
282310
283- < form onSubmit = { save } class = "flex flex-col gap-6" >
284- < div class = "grid grid-cols-1 gap-4" >
285- < TextField
286- autofocus
287- label = "Provider ID"
288- placeholder = "myprovider"
289- value = { form . providerID }
290- onChange = { setForm . bind ( null , "providerID" ) }
291- validationState = { errors . providerID ? "invalid" : undefined }
292- error = { errors . providerID }
293- />
294- < TextField
295- label = "Display name"
296- placeholder = "My AI Provider"
297- value = { form . name }
298- onChange = { setForm . bind ( null , "name" ) }
299- validationState = { errors . name ? "invalid" : undefined }
300- error = { errors . name }
301- />
302- < TextField
303- label = "Base URL"
304- placeholder = "https://api.myprovider.com/v1"
305- value = { form . baseURL }
306- onChange = { setForm . bind ( null , "baseURL" ) }
307- validationState = { errors . baseURL ? "invalid" : undefined }
308- error = { errors . baseURL }
309- />
310- < TextField
311- label = "API key (optional)"
312- placeholder = "{env:MYPROVIDER_API_KEY}"
313- description = "Leave empty if you manage auth elsewhere."
314- value = { form . apiKey }
315- onChange = { setForm . bind ( null , "apiKey" ) }
316- />
317- </ div >
318-
319- < div class = "flex flex-col gap-3" >
320- < div class = "text-14-medium text-text-strong" > Models</ div >
321- < For each = { form . models } >
322- { ( m , i ) => (
323- < div class = "flex gap-3 items-start" >
324- < div class = "flex-1 grid grid-cols-1 gap-3" >
325- < TextField
326- label = { i ( ) === 0 ? "Model ID" : undefined }
327- hideLabel = { i ( ) !== 0 }
328- placeholder = "my-model-name"
329- value = { m . id }
330- onChange = { ( v ) => setForm ( "models" , i ( ) , "id" , v ) }
331- validationState = { errors . models [ i ( ) ] ?. id ? "invalid" : undefined }
332- error = { errors . models [ i ( ) ] ?. id }
333- />
334- < TextField
335- label = { i ( ) === 0 ? "Model name" : undefined }
336- hideLabel = { i ( ) !== 0 }
337- placeholder = "My Model"
338- value = { m . name }
339- onChange = { ( v ) => setForm ( "models" , i ( ) , "name" , v ) }
340- validationState = { errors . models [ i ( ) ] ?. name ? "invalid" : undefined }
341- error = { errors . models [ i ( ) ] ?. name }
342- />
343- </ div >
344- < IconButton
345- type = "button"
346- icon = "trash"
347- variant = "ghost"
348- onClick = { ( ) => removeModel ( i ( ) ) }
349- aria-label = "Remove model"
311+ < div class = "flex flex-col gap-3" >
312+ < label class = "text-12-medium text-text-weak" > Models</ label >
313+ < For each = { form . models } >
314+ { ( m , i ) => (
315+ < div class = "flex gap-2 items-start" >
316+ < div class = "flex-1" >
317+ < TextField
318+ label = "ID"
319+ hideLabel
320+ placeholder = "model-id"
321+ value = { m . id }
322+ onChange = { ( v ) => setForm ( "models" , i ( ) , "id" , v ) }
323+ validationState = { errors . models [ i ( ) ] ?. id ? "invalid" : undefined }
324+ error = { errors . models [ i ( ) ] ?. id }
350325 />
351326 </ div >
352- ) }
353- </ For >
354- < Button type = "button" size = "large" variant = "secondary" icon = "plus-small" onClick = { addModel } >
355- Add model
356- </ Button >
357- </ div >
358-
359- < div class = "flex flex-col gap-3" >
360- < div class = "text-14-medium text-text-strong" > Headers (optional)</ div >
361- < For each = { form . headers } >
362- { ( h , i ) => (
363- < div class = "flex gap-3 items-start" >
364- < div class = "flex-1 grid grid-cols-1 gap-3" >
365- < TextField
366- label = { i ( ) === 0 ? "Header" : undefined }
367- hideLabel = { i ( ) !== 0 }
368- placeholder = "Authorization"
369- value = { h . key }
370- onChange = { ( v ) => setForm ( "headers" , i ( ) , "key" , v ) }
371- validationState = { errors . headers [ i ( ) ] ?. key ? "invalid" : undefined }
372- error = { errors . headers [ i ( ) ] ?. key }
373- />
374- < TextField
375- label = { i ( ) === 0 ? "Value" : undefined }
376- hideLabel = { i ( ) !== 0 }
377- placeholder = "Bearer ..."
378- value = { h . value }
379- onChange = { ( v ) => setForm ( "headers" , i ( ) , "value" , v ) }
380- validationState = { errors . headers [ i ( ) ] ?. value ? "invalid" : undefined }
381- error = { errors . headers [ i ( ) ] ?. value }
382- />
383- </ div >
384- < IconButton
385- type = "button"
386- icon = "trash"
387- variant = "ghost"
388- onClick = { ( ) => removeHeader ( i ( ) ) }
389- aria-label = "Remove header"
327+ < div class = "flex-1" >
328+ < TextField
329+ label = "Name"
330+ hideLabel
331+ placeholder = "Display Name"
332+ value = { m . name }
333+ onChange = { ( v ) => setForm ( "models" , i ( ) , "name" , v ) }
334+ validationState = { errors . models [ i ( ) ] ?. name ? "invalid" : undefined }
335+ error = { errors . models [ i ( ) ] ?. name }
390336 />
391337 </ div >
392- ) }
393- </ For >
394- < Button type = "button" size = "large" variant = "secondary" icon = "plus-small" onClick = { addHeader } >
395- Add header
396- </ Button >
397- </ div >
398-
399- < div class = "flex items-center gap-3" >
400- < Button
401- type = "button"
402- size = "large"
403- variant = "secondary"
404- onClick = { ( ) => dialog . close ( ) }
405- disabled = { form . saving }
406- >
407- { language . t ( "common.cancel" ) }
408- </ Button >
409- < Button type = "submit" size = "large" variant = "primary" disabled = { form . saving } >
410- { form . saving ? language . t ( "common.saving" ) : language . t ( "common.save" ) }
411- </ Button >
412- </ div >
413- </ form >
414- </ div >
338+ < IconButton
339+ type = "button"
340+ icon = "trash"
341+ variant = "ghost"
342+ class = "mt-1.5"
343+ onClick = { ( ) => removeModel ( i ( ) ) }
344+ disabled = { form . models . length <= 1 }
345+ aria-label = "Remove model"
346+ />
347+ </ div >
348+ ) }
349+ </ For >
350+ < Button type = "button" size = "small" variant = "ghost" icon = "plus-small" onClick = { addModel } class = "self-start" >
351+ Add model
352+ </ Button >
353+ </ div >
354+
355+ < div class = "flex flex-col gap-3" >
356+ < label class = "text-12-medium text-text-weak" > Headers (optional)</ label >
357+ < For each = { form . headers } >
358+ { ( h , i ) => (
359+ < div class = "flex gap-2 items-start" >
360+ < div class = "flex-1" >
361+ < TextField
362+ label = "Header"
363+ hideLabel
364+ placeholder = "Header-Name"
365+ value = { h . key }
366+ onChange = { ( v ) => setForm ( "headers" , i ( ) , "key" , v ) }
367+ validationState = { errors . headers [ i ( ) ] ?. key ? "invalid" : undefined }
368+ error = { errors . headers [ i ( ) ] ?. key }
369+ />
370+ </ div >
371+ < div class = "flex-1" >
372+ < TextField
373+ label = "Value"
374+ hideLabel
375+ placeholder = "value"
376+ value = { h . value }
377+ onChange = { ( v ) => setForm ( "headers" , i ( ) , "value" , v ) }
378+ validationState = { errors . headers [ i ( ) ] ?. value ? "invalid" : undefined }
379+ error = { errors . headers [ i ( ) ] ?. value }
380+ />
381+ </ div >
382+ < IconButton
383+ type = "button"
384+ icon = "trash"
385+ variant = "ghost"
386+ class = "mt-1.5"
387+ onClick = { ( ) => removeHeader ( i ( ) ) }
388+ disabled = { form . headers . length <= 1 }
389+ aria-label = "Remove header"
390+ />
391+ </ div >
392+ ) }
393+ </ For >
394+ < Button type = "button" size = "small" variant = "ghost" icon = "plus-small" onClick = { addHeader } class = "self-start" >
395+ Add header
396+ </ Button >
397+ </ div >
398+
399+ < Button class = "w-auto self-start" type = "submit" size = "large" variant = "primary" disabled = { form . saving } >
400+ { form . saving ? "Saving..." : language . t ( "common.submit" ) }
401+ </ Button >
402+ </ form >
415403 </ div >
416404 </ Dialog >
417405 )
0 commit comments