fix: refresh provider settings on profile switch in Settings view#11460
fix: refresh provider settings on profile switch in Settings view#11460roomote[bot] wants to merge 1 commit intomainfrom
Conversation
When switching Configuration Profiles within the Settings/Providers tab,
provider settings were not refreshing because:
1. The cachedState update happened asynchronously (useEffect) after the
render cycle, causing child components to mount with stale config
2. ApiOptions and provider components retained local state from the
previous profile since they were not remounted
Three fixes applied:
- Changed profile-switch useEffect to synchronous render-time state
update (React "adjusting state when a prop changes" pattern)
- Added key={currentApiConfigName} on ApiOptions to force full remount
on profile switch, resetting all local state in provider components
- Added sync effects and write-back guards in OpenAICompatible.tsx to
prevent stale header overwrites
Closes #11450
The core approach (synchronous render-time state update + key-based remount) is sound and correctly solves the profile-switch staleness issue. Two items flagged:
Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues. |
| // Sync local customHeaders state when apiConfiguration changes externally | ||
| // (e.g., on profile switch). This mirrors the pattern in ApiOptions.tsx. | ||
| useEffect(() => { | ||
| const propHeaders = apiConfiguration?.openAiHeaders || {} | ||
| if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) { | ||
| setCustomHeaders(Object.entries(propHeaders)) | ||
| } | ||
| }, [apiConfiguration?.openAiHeaders]) // eslint-disable-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
These sync effects are redundant for the profile-switch scenario described in the PR. The key={currentApiConfigName} on ApiOptions in SettingsView.tsx already causes OpenAICompatible to fully unmount and remount when the profile changes, so the useState initializer at line 50 reads fresh values from the new apiConfiguration directly. The sync effects would only fire if openAiHeaders changed without a currentApiConfigName change (no remount).
Also, the comment says this "mirrors the pattern in ApiOptions.tsx," but the ApiOptions version at line 146 includes customHeaders in its dependency array whereas this version omits it with an eslint-disable. Consider either removing these effects if the key-based remount is the intended mechanism, or updating the comments to clarify they're defensive measures for non-profile-switch config changes.
Fix it with Roo Code or mention @roomote and request a fix.
| const writeBackCalls = mockSetApiConfigurationField.mock.calls.filter( | ||
| (call: any[]) => call[0] === "openAiHeaders", | ||
| ) | ||
| // If there are write-back calls, they should be writing the NEW headers, not the old ones | ||
| for (const call of writeBackCalls) { | ||
| const writtenHeaders = call[1] as Record<string, string> | ||
| expect(writtenHeaders).not.toEqual({ "X-Profile": "old-profile" }) | ||
| } |
There was a problem hiding this comment.
This test uses rerender to simulate a profile switch, but in production the key={currentApiConfigName} on ApiOptions causes OpenAICompatible to be unmounted and remounted -- not re-rendered in place. The test therefore exercises the sync effect path (lines 57-62) rather than the actual profile-switch code path.
Additionally, when the sync + guard work correctly, writeBackCalls is empty and the for loop body never executes, making the assertion vacuously true. Consider adding an explicit expect(writeBackCalls).toHaveLength(0) to make the expected outcome clear and to catch regressions where a stale write-back does fire.
| const writeBackCalls = mockSetApiConfigurationField.mock.calls.filter( | |
| (call: any[]) => call[0] === "openAiHeaders", | |
| ) | |
| // If there are write-back calls, they should be writing the NEW headers, not the old ones | |
| for (const call of writeBackCalls) { | |
| const writtenHeaders = call[1] as Record<string, string> | |
| expect(writtenHeaders).not.toEqual({ "X-Profile": "old-profile" }) | |
| } | |
| const writeBackCalls = mockSetApiConfigurationField.mock.calls.filter( | |
| (call: any[]) => call[0] === "openAiHeaders", | |
| ) | |
| // No write-back should occur since local state is synced to new headers | |
| expect(writeBackCalls).toHaveLength(0) |
Fix it with Roo Code or mention @roomote and request a fix.
Related GitHub Issue
Closes: #11450
Description
This PR attempts to address Issue #11450 where switching Configuration Profiles within the Settings/Providers tab did not refresh the provider settings content.
Root Cause: When switching profiles via the dropdown in Settings, the
cachedStateupdate happened asynchronously (viauseEffect) after the render cycle. This meant child components likeApiOptionsand their provider sub-components would either:Three fixes applied:
Synchronous render-time state update (
SettingsView.tsx): Changed the profile-switchuseEffectto a synchronous state update during render (the React-recommended "adjusting state when a prop changes" pattern). This ensurescachedStateis updated before child components render, so they receive the correctapiConfigurationimmediately.Key-based remount (
SettingsView.tsx): Addedkey={currentApiConfigName}on theApiOptionscomponent, forcing React to completely unmount and remount the entire provider settings tree when the profile changes. This resets all local state inApiOptionsand every provider component (OpenAICompatible, Anthropic, OpenRouter, etc.).Defensive sync and write-back guard (
OpenAICompatible.tsx): Added a sync effect forcustomHeadersandazureApiVersionSelectedto handle external changes, and added a guard to the debounced write-back effect to prevent stale header overwrites.Feedback and guidance are welcome.
Test Procedure
OpenAICompatible.spec.tsx:Pre-Submission Checklist
Documentation Updates
Additional Notes
The synchronous render-time state update pattern follows the React docs recommendation for "adjusting state when a prop changes." This is preferred over
useEffectbecause it ensures the state update happens before the render commits, avoiding a frame of stale UI.