@@ -2,13 +2,67 @@ import React, { useState, useEffect, useCallback } from "react";
22import { ChevronDown , ChevronRight , Check , X } from "lucide-react" ;
33import type { ProvidersConfigMap } from "../types" ;
44import { SUPPORTED_PROVIDERS , PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers" ;
5+ import type { ProviderName } from "@/common/constants/providers" ;
6+
7+ interface FieldConfig {
8+ key : string ;
9+ label : string ;
10+ placeholder : string ;
11+ type : "secret" | "text" ;
12+ optional ?: boolean ;
13+ }
14+
15+ /**
16+ * Get provider-specific field configuration.
17+ * Most providers use API Key + Base URL, but some (like Bedrock) have different needs.
18+ */
19+ function getProviderFields ( provider : ProviderName ) : FieldConfig [ ] {
20+ if ( provider === "bedrock" ) {
21+ return [
22+ { key : "region" , label : "Region" , placeholder : "us-east-1" , type : "text" } ,
23+ {
24+ key : "bearerToken" ,
25+ label : "Bearer Token" ,
26+ placeholder : "AWS_BEARER_TOKEN_BEDROCK" ,
27+ type : "secret" ,
28+ optional : true ,
29+ } ,
30+ {
31+ key : "accessKeyId" ,
32+ label : "Access Key ID" ,
33+ placeholder : "AWS Access Key ID" ,
34+ type : "secret" ,
35+ optional : true ,
36+ } ,
37+ {
38+ key : "secretAccessKey" ,
39+ label : "Secret Access Key" ,
40+ placeholder : "AWS Secret Access Key" ,
41+ type : "secret" ,
42+ optional : true ,
43+ } ,
44+ ] ;
45+ }
46+
47+ // Default for most providers
48+ return [
49+ { key : "apiKey" , label : "API Key" , placeholder : "Enter API key" , type : "secret" } ,
50+ {
51+ key : "baseUrl" ,
52+ label : "Base URL" ,
53+ placeholder : "https://api.example.com" ,
54+ type : "text" ,
55+ optional : true ,
56+ } ,
57+ ] ;
58+ }
559
660export function ProvidersSection ( ) {
761 const [ config , setConfig ] = useState < ProvidersConfigMap > ( { } ) ;
862 const [ expandedProvider , setExpandedProvider ] = useState < string | null > ( null ) ;
963 const [ editingField , setEditingField ] = useState < {
1064 provider : string ;
11- field : "apiKey" | "baseUrl" ;
65+ field : string ;
1266 } | null > ( null ) ;
1367 const [ editValue , setEditValue ] = useState ( "" ) ;
1468 const [ saving , setSaving ] = useState ( false ) ;
@@ -26,11 +80,14 @@ export function ProvidersSection() {
2680 setEditingField ( null ) ;
2781 } ;
2882
29- const handleStartEdit = ( provider : string , field : "apiKey" | "baseUrl" ) => {
83+ const handleStartEdit = ( provider : string , field : string , fieldConfig : FieldConfig ) => {
3084 setEditingField ( { provider, field } ) ;
31- // For API key, start empty since we only show masked value
32- // For baseUrl, show current value
33- setEditValue ( field === "baseUrl" ? ( config [ provider ] ?. baseUrl ?? "" ) : "" ) ;
85+ // For secrets, start empty since we only show masked value
86+ // For text fields, show current value
87+ const currentValue = ( config [ provider ] as Record < string , unknown > | undefined ) ?. [ field ] ;
88+ setEditValue (
89+ fieldConfig . type === "text" && typeof currentValue === "string" ? currentValue : ""
90+ ) ;
3491 } ;
3592
3693 const handleCancelEdit = ( ) => {
@@ -44,8 +101,7 @@ export function ProvidersSection() {
44101 setSaving ( true ) ;
45102 try {
46103 const { provider, field } = editingField ;
47- const keyPath = field === "apiKey" ? [ "apiKey" ] : [ "baseUrl" ] ;
48- await window . api . providers . setProviderConfig ( provider , keyPath , editValue ) ;
104+ await window . api . providers . setProviderConfig ( provider , [ field ] , editValue ) ;
49105
50106 // Refresh config
51107 const cfg = await window . api . providers . getConfig ( ) ;
@@ -57,10 +113,10 @@ export function ProvidersSection() {
57113 }
58114 } , [ editingField , editValue ] ) ;
59115
60- const handleClearBaseUrl = useCallback ( async ( provider : string ) => {
116+ const handleClearField = useCallback ( async ( provider : string , field : string ) => {
61117 setSaving ( true ) ;
62118 try {
63- await window . api . providers . setProviderConfig ( provider , [ "baseUrl" ] , "" ) ;
119+ await window . api . providers . setProviderConfig ( provider , [ field ] , "" ) ;
64120 const cfg = await window . api . providers . getConfig ( ) ;
65121 setConfig ( cfg ) ;
66122 } finally {
@@ -72,6 +128,25 @@ export function ProvidersSection() {
72128 return config [ provider ] ?. apiKeySet ?? false ;
73129 } ;
74130
131+ const getFieldValue = ( provider : string , field : string ) : string | undefined => {
132+ const providerConfig = config [ provider ] as Record < string , unknown > | undefined ;
133+ if ( ! providerConfig ) return undefined ;
134+ const value = providerConfig [ field ] ;
135+ return typeof value === "string" ? value : undefined ;
136+ } ;
137+
138+ const isFieldSet = ( provider : string , field : string , fieldConfig : FieldConfig ) : boolean => {
139+ if ( fieldConfig . type === "secret" ) {
140+ // For apiKey, we have apiKeySet from the sanitized config
141+ if ( field === "apiKey" ) return config [ provider ] ?. apiKeySet ?? false ;
142+ // For other secrets, check if the field exists in the raw config
143+ // Since we don't expose secret values, we assume they're not set if undefined
144+ const providerConfig = config [ provider ] as Record < string , unknown > | undefined ;
145+ return providerConfig ?. [ `${ field } Set` ] === true ;
146+ }
147+ return ! ! getFieldValue ( provider , field ) ;
148+ } ;
149+
75150 return (
76151 < div className = "space-y-2" >
77152 < p className = "text-muted mb-4 text-xs" >
@@ -81,8 +156,8 @@ export function ProvidersSection() {
81156
82157 { SUPPORTED_PROVIDERS . map ( ( provider ) => {
83158 const isExpanded = expandedProvider === provider ;
84- const providerConfig = config [ provider ] ;
85159 const configured = isConfigured ( provider ) ;
160+ const fields = getProviderFields ( provider ) ;
86161
87162 return (
88163 < div
@@ -114,117 +189,83 @@ export function ProvidersSection() {
114189 { /* Provider settings */ }
115190 { isExpanded && (
116191 < div className = "border-border-medium space-y-3 border-t px-4 py-3" >
117- { /* API Key */ }
118- < div >
119- < label className = "text-muted mb-1 block text-xs" > API Key</ label >
120- { editingField ?. provider === provider && editingField ?. field === "apiKey" ? (
121- < div className = "flex gap-2" >
122- < input
123- type = "password"
124- value = { editValue }
125- onChange = { ( e ) => setEditValue ( e . target . value ) }
126- placeholder = "Enter API key"
127- className = "bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
128- autoFocus
129- onKeyDown = { ( e ) => {
130- if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
131- if ( e . key === "Escape" ) handleCancelEdit ( ) ;
132- } }
133- />
134- < button
135- type = "button"
136- onClick = { ( ) => void handleSaveEdit ( ) }
137- disabled = { saving }
138- className = "p-1 text-green-500 hover:text-green-400"
139- >
140- < Check className = "h-4 w-4" />
141- </ button >
142- < button
143- type = "button"
144- onClick = { handleCancelEdit }
145- className = "text-muted hover:text-foreground p-1"
146- >
147- < X className = "h-4 w-4" />
148- </ button >
149- </ div >
150- ) : (
151- < div className = "flex items-center justify-between" >
152- < span className = "text-foreground font-mono text-xs" >
153- { providerConfig ?. apiKeySet ? "••••••••" : "Not set" }
154- </ span >
155- < button
156- type = "button"
157- onClick = { ( ) => handleStartEdit ( provider , "apiKey" ) }
158- className = "text-accent hover:text-accent-light text-xs"
159- >
160- { providerConfig ?. apiKeySet ? "Change" : "Set" }
161- </ button >
162- </ div >
163- ) }
164- </ div >
165-
166- { /* Base URL (optional) */ }
167- < div >
168- < label className = "text-muted mb-1 block text-xs" >
169- Base URL < span className = "text-dim" > (optional)</ span >
170- </ label >
171- { editingField ?. provider === provider && editingField ?. field === "baseUrl" ? (
172- < div className = "flex gap-2" >
173- < input
174- type = "text"
175- value = { editValue }
176- onChange = { ( e ) => setEditValue ( e . target . value ) }
177- placeholder = "https://api.example.com"
178- className = "bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
179- autoFocus
180- onKeyDown = { ( e ) => {
181- if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
182- if ( e . key === "Escape" ) handleCancelEdit ( ) ;
183- } }
184- />
185- < button
186- type = "button"
187- onClick = { ( ) => void handleSaveEdit ( ) }
188- disabled = { saving }
189- className = "p-1 text-green-500 hover:text-green-400"
190- >
191- < Check className = "h-4 w-4" />
192- </ button >
193- < button
194- type = "button"
195- onClick = { handleCancelEdit }
196- className = "text-muted hover:text-foreground p-1"
197- >
198- < X className = "h-4 w-4" />
199- </ button >
200- </ div >
201- ) : (
202- < div className = "flex items-center justify-between" >
203- < span className = "text-foreground font-mono text-xs" >
204- { providerConfig ?. baseUrl ?? "Default" }
205- </ span >
206- < div className = "flex gap-2" >
207- { providerConfig ?. baseUrl && (
192+ { fields . map ( ( fieldConfig ) => {
193+ const isEditing =
194+ editingField ?. provider === provider && editingField ?. field === fieldConfig . key ;
195+ const fieldValue = getFieldValue ( provider , fieldConfig . key ) ;
196+ const fieldIsSet = isFieldSet ( provider , fieldConfig . key , fieldConfig ) ;
197+
198+ return (
199+ < div key = { fieldConfig . key } >
200+ < label className = "text-muted mb-1 block text-xs" >
201+ { fieldConfig . label }
202+ { fieldConfig . optional && < span className = "text-dim" > (optional)</ span > }
203+ </ label >
204+ { isEditing ? (
205+ < div className = "flex gap-2" >
206+ < input
207+ type = { fieldConfig . type === "secret" ? "password" : "text" }
208+ value = { editValue }
209+ onChange = { ( e ) => setEditValue ( e . target . value ) }
210+ placeholder = { fieldConfig . placeholder }
211+ className = "bg-modal-bg border-border-medium focus:border-accent flex-1 rounded border px-2 py-1.5 font-mono text-xs focus:outline-none"
212+ autoFocus
213+ onKeyDown = { ( e ) => {
214+ if ( e . key === "Enter" ) void handleSaveEdit ( ) ;
215+ if ( e . key === "Escape" ) handleCancelEdit ( ) ;
216+ } }
217+ />
208218 < button
209219 type = "button"
210- onClick = { ( ) => void handleClearBaseUrl ( provider ) }
220+ onClick = { ( ) => void handleSaveEdit ( ) }
211221 disabled = { saving }
212- className = "text-muted hover:text-error text-xs"
222+ className = "p-1 text-green-500 hover:text-green-400"
223+ >
224+ < Check className = "h-4 w-4" />
225+ </ button >
226+ < button
227+ type = "button"
228+ onClick = { handleCancelEdit }
229+ className = "text-muted hover:text-foreground p-1"
213230 >
214- Clear
231+ < X className = "h-4 w-4" />
215232 </ button >
216- ) }
217- < button
218- type = "button"
219- onClick = { ( ) => handleStartEdit ( provider , "baseUrl" ) }
220- className = "text-accent hover:text-accent-light text-xs"
221- >
222- { providerConfig ?. baseUrl ? "Change" : "Set" }
223- </ button >
224- </ div >
233+ </ div >
234+ ) : (
235+ < div className = "flex items-center justify-between" >
236+ < span className = "text-foreground font-mono text-xs" >
237+ { fieldConfig . type === "secret"
238+ ? fieldIsSet
239+ ? "••••••••"
240+ : "Not set"
241+ : ( fieldValue ?? "Default" ) }
242+ </ span >
243+ < div className = "flex gap-2" >
244+ { fieldConfig . type === "text" && fieldValue && (
245+ < button
246+ type = "button"
247+ onClick = { ( ) => void handleClearField ( provider , fieldConfig . key ) }
248+ disabled = { saving }
249+ className = "text-muted hover:text-error text-xs"
250+ >
251+ Clear
252+ </ button >
253+ ) }
254+ < button
255+ type = "button"
256+ onClick = { ( ) =>
257+ handleStartEdit ( provider , fieldConfig . key , fieldConfig )
258+ }
259+ className = "text-accent hover:text-accent-light text-xs"
260+ >
261+ { fieldIsSet || fieldValue ? "Change" : "Set" }
262+ </ button >
263+ </ div >
264+ </ div >
265+ ) }
225266 </ div >
226- ) }
227- </ div >
267+ ) ;
268+ } ) }
228269 </ div >
229270 ) }
230271 </ div >
0 commit comments