Skip to content

Commit e5ad879

Browse files
committed
PR feedback
1 parent eb6d3d7 commit e5ad879

File tree

1 file changed

+80
-53
lines changed

1 file changed

+80
-53
lines changed

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 80 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,83 @@ const ApiOptions = ({
8080

8181
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
8282

83+
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
84+
const headers = apiConfiguration?.openAiHeaders || {}
85+
return Object.entries(headers)
86+
})
87+
8388
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
8489
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
8590
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
8691
const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat)
8792
const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState(
8893
!!apiConfiguration?.googleGeminiBaseUrl,
8994
)
95+
96+
const handleAddCustomHeader = useCallback(() => {
97+
// Only update the local state to show the new row in the UI
98+
setCustomHeaders((prev) => [...prev, ["", ""]])
99+
// Do not update the main configuration yet, wait for user input
100+
}, [])
101+
102+
const handleUpdateHeaderKey = useCallback((index: number, newKey: string) => {
103+
setCustomHeaders((prev) => {
104+
const updated = [...prev]
105+
if (updated[index]) {
106+
updated[index] = [newKey, updated[index][1]]
107+
}
108+
return updated
109+
})
110+
}, [])
111+
112+
const handleUpdateHeaderValue = useCallback((index: number, newValue: string) => {
113+
setCustomHeaders((prev) => {
114+
const updated = [...prev]
115+
if (updated[index]) {
116+
updated[index] = [updated[index][0], newValue]
117+
}
118+
return updated
119+
})
120+
}, [])
121+
122+
const handleRemoveCustomHeader = useCallback((index: number) => {
123+
setCustomHeaders((prev) => prev.filter((_, i) => i !== index))
124+
}, [])
125+
126+
// Helper to convert array of tuples to object (filtering out empty keys)
127+
const convertHeadersToObject = (headers: [string, string][]): Record<string, string> => {
128+
const result: Record<string, string> = {}
129+
130+
// Process each header tuple
131+
for (const [key, value] of headers) {
132+
const trimmedKey = key.trim()
133+
134+
// Skip empty keys
135+
if (!trimmedKey) continue
136+
137+
// For duplicates, the last one in the array wins
138+
// This matches how HTTP headers work in general
139+
result[trimmedKey] = value.trim()
140+
}
141+
142+
return result
143+
}
144+
145+
// Debounced effect to update the main configuration when local customHeaders state stabilizes
146+
useDebounce(
147+
() => {
148+
const currentConfigHeaders = apiConfiguration?.openAiHeaders || {}
149+
const newHeadersObject = convertHeadersToObject(customHeaders)
150+
151+
// Only update if the processed object is different from the current config
152+
if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) {
153+
setApiConfigurationField("openAiHeaders", newHeadersObject)
154+
}
155+
},
156+
300,
157+
[customHeaders, apiConfiguration?.openAiHeaders, setApiConfigurationField],
158+
)
159+
90160
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
91161
const noTransform = <T,>(value: T) => value
92162

@@ -123,13 +193,15 @@ const ApiOptions = ({
123193
useDebounce(
124194
() => {
125195
if (selectedProvider === "openai") {
196+
// Use our custom headers state to build the headers object
197+
const headerObject = convertHeadersToObject(customHeaders)
126198
vscode.postMessage({
127199
type: "requestOpenAiModels",
128200
values: {
129201
baseUrl: apiConfiguration?.openAiBaseUrl,
130202
apiKey: apiConfiguration?.openAiApiKey,
131203
customHeaders: {}, // Reserved for any additional headers
132-
openAiHeaders: apiConfiguration?.openAiHeaders,
204+
openAiHeaders: headerObject,
133205
},
134206
})
135207
} else if (selectedProvider === "ollama") {
@@ -148,6 +220,7 @@ const ApiOptions = ({
148220
apiConfiguration?.openAiApiKey,
149221
apiConfiguration?.ollamaBaseUrl,
150222
apiConfiguration?.lmStudioBaseUrl,
223+
customHeaders,
151224
],
152225
)
153226

@@ -842,79 +915,33 @@ const ApiOptions = ({
842915
<VSCodeButton
843916
appearance="icon"
844917
title={t("settings:common.add")}
845-
onClick={() => {
846-
const currentHeaders = { ...(apiConfiguration?.openAiHeaders || {}) }
847-
// Use an empty string as key - user will fill it in
848-
// The key will be properly set when the user types in the field
849-
currentHeaders[""] = ""
850-
handleInputChange("openAiHeaders")({
851-
target: {
852-
value: currentHeaders,
853-
},
854-
})
855-
}}>
918+
onClick={handleAddCustomHeader}>
856919
<span className="codicon codicon-add"></span>
857920
</VSCodeButton>
858921
</div>
859-
{Object.keys(apiConfiguration?.openAiHeaders || {}).length === 0 ? (
922+
{!customHeaders.length ? (
860923
<div className="text-sm text-vscode-descriptionForeground">
861924
{t("settings:providers.noCustomHeaders")}
862925
</div>
863926
) : (
864-
Object.entries(apiConfiguration?.openAiHeaders || {}).map(([key, value], index) => (
927+
customHeaders.map(([key, value], index) => (
865928
<div key={index} className="flex items-center mb-2">
866929
<VSCodeTextField
867930
value={key}
868931
className="flex-1 mr-2"
869932
placeholder={t("settings:providers.headerName")}
870-
onInput={(e: any) => {
871-
const currentHeaders = apiConfiguration?.openAiHeaders ?? {}
872-
const newValue = e.target.value
873-
874-
if (newValue && newValue !== key) {
875-
const { [key]: _, ...rest } = currentHeaders
876-
handleInputChange("openAiHeaders")({
877-
target: {
878-
value: {
879-
...rest,
880-
[newValue]: value,
881-
},
882-
},
883-
})
884-
}
885-
}}
933+
onInput={(e: any) => handleUpdateHeaderKey(index, e.target.value)}
886934
/>
887935
<VSCodeTextField
888936
value={value}
889937
className="flex-1 mr-2"
890938
placeholder={t("settings:providers.headerValue")}
891-
onInput={(e: any) => {
892-
handleInputChange("openAiHeaders")({
893-
target: {
894-
value: {
895-
...(apiConfiguration?.openAiHeaders ?? {}),
896-
[key]: e.target.value,
897-
},
898-
},
899-
})
900-
}}
939+
onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
901940
/>
902941
<VSCodeButton
903942
appearance="icon"
904943
title={t("settings:common.remove")}
905-
onClick={() => {
906-
const { [key]: _, ...rest } = apiConfiguration?.openAiHeaders ?? {}
907-
908-
// Ensure we always maintain an empty object even when removing the last header
909-
// This prevents the field from becoming undefined or null
910-
const newHeaders = Object.keys(rest).length === 0 ? {} : rest
911-
912-
handleInputChange("openAiHeaders")({
913-
target: {
914-
value: newHeaders,
915-
},
916-
})
917-
}}>
944+
onClick={() => handleRemoveCustomHeader(index)}>
918945
<span className="codicon codicon-trash"></span>
919946
</VSCodeButton>
920947
</div>

0 commit comments

Comments
 (0)