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
This commit is contained in:
Roo Code 2026-01-06 21:08:40 +00:00
parent 503f40241d
commit 3d3b8c411e
2 changed files with 64 additions and 4 deletions

View file

@ -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(
<K extends keyof ProviderSettings, E>(

View file

@ -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<ProviderSettings> = {
openAiHeaders: { "X-Test": "value" },
}
render(
<OpenAICompatible
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
organizationAllowList={mockOrganizationAllowList}
/>,
)
// 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<ProviderSettings> = {
openAiHeaders: {},
}
render(
<OpenAICompatible
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
organizationAllowList={mockOrganizationAllowList}
/>,
)
// 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())
})
})