From 3d3b8c411e168b2ce42132d85aac3f8d17e2d17c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 6 Jan 2026 21:08:40 +0000 Subject: [PATCH] fix: prevent false dirty state from headers in OpenAICompatible Add deep equality check before calling setApiConfigurationField for openAiHeaders in OpenAICompatible.tsx. This prevents the Settings dialog from incorrectly showing an unsaved changes prompt when the headers have not actually changed. The fix follows the same pattern already used in ApiOptions.tsx, using JSON.stringify for deep equality comparison. Fixes #8230 --- .../settings/providers/OpenAICompatible.tsx | 11 ++-- .../__tests__/OpenAICompatible.spec.tsx | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) 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()) + }) +})