diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index ad338d342ab..3e3526fc5da 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -84,17 +84,20 @@ export const OpenAICompatible = ({ setCustomHeaders((prev) => prev.filter((_, i) => i !== index)) }, []) - // Helper to convert array of tuples to object - // Add effect to update the parent component's state when local headers change useEffect(() => { const timer = setTimeout(() => { + const currentConfigHeaders = apiConfiguration?.openAiHeaders || {} const headerObject = convertHeadersToObject(customHeaders) - setApiConfigurationField("openAiHeaders", headerObject) + + // Only update if the processed object is different from the current config. + if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(headerObject)) { + setApiConfigurationField("openAiHeaders", headerObject) + } }, 300) return () => clearTimeout(timer) - }, [customHeaders, setApiConfigurationField]) + }, [customHeaders, apiConfiguration?.openAiHeaders, setApiConfigurationField]) const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx index aba81ec2191..b974dcf348c 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx @@ -313,3 +313,60 @@ describe("OpenAICompatible Component - includeMaxTokens checkbox", () => { }) }) }) + +describe("OpenAICompatible Component - Headers dirty state fix", () => { + const mockSetApiConfigurationField = vi.fn() + const mockOrganizationAllowList = { + allowAll: true, + providers: {}, + } + + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("should not call setApiConfigurationField when headers have not changed", async () => { + const apiConfiguration: Partial = { + openAiHeaders: { "X-Test": "value" }, + } + + render( + , + ) + + // Wait for the debounced update + vi.advanceTimersByTime(350) + + // setApiConfigurationField should NOT be called because the headers haven't changed + expect(mockSetApiConfigurationField).not.toHaveBeenCalledWith("openAiHeaders", expect.anything()) + }) + + it("should not trigger dirty state on initial mount with empty headers", async () => { + const apiConfiguration: Partial = { + openAiHeaders: {}, + } + + render( + , + ) + + // Wait for the debounced update + vi.advanceTimersByTime(350) + + // setApiConfigurationField should NOT be called because the headers haven't changed + expect(mockSetApiConfigurationField).not.toHaveBeenCalledWith("openAiHeaders", expect.anything()) + }) +})