From cc47997ca7142b3dc06229a87ef118dac17bd006 Mon Sep 17 00:00:00 2001 From: Robert McIntyre Date: Sat, 7 Feb 2026 18:21:12 -0800 Subject: [PATCH] Fixes unsaved changes prompt on unchanged settings (#8230). --- .../src/components/settings/ApiOptions.tsx | 3 ++- .../src/components/settings/SettingsView.tsx | 21 +++++++++++++++++-- .../settings/providers/OpenAICompatible.tsx | 9 ++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 939d2734d4b..7134985e9c4 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -171,7 +171,8 @@ const ApiOptions = ({ // Only update if the processed object is different from the current config. if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) { - setApiConfigurationField("openAiHeaders", newHeadersObject) + // Pass false to indicate this is automatic sync, not a user action + setApiConfigurationField("openAiHeaders", newHeadersObject, false) } }, 300, diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 444336db8e9..3765c297a0c 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -256,9 +256,23 @@ const SettingsView = forwardRef(({ onDone, t const previousValue = prevState.apiConfiguration?.[field] + // Helper to check if two values are semantically equal + const areValuesEqual = (a: any, b: any): boolean => { + // Same reference + if (a === b) return true + // Both null/undefined + if (a == null && b == null) return true + // Different types + if (typeof a !== typeof b) return false + // For objects/arrays, do deep comparison via JSON (good enough for settings) + if (typeof a === "object" && typeof b === "object") { + return JSON.stringify(a) === JSON.stringify(b) + } + return false + } + // Only skip change detection for automatic initialization (not user actions) // This prevents the dirty state when the component initializes and auto-syncs values - // Treat undefined, null, and empty string as uninitialized states const isInitialSync = !isUserAction && (previousValue === undefined || previousValue === "" || previousValue === null) && @@ -266,7 +280,10 @@ const SettingsView = forwardRef(({ onDone, t value !== "" && value !== null - if (!isInitialSync) { + // Also skip if it's an automatic sync with semantically equal values + const isAutomaticNoOpSync = !isUserAction && areValuesEqual(previousValue, value) + + if (!isInitialSync && !isAutomaticNoOpSync) { setChangeDetected(true) } return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } } diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 4eea6f09f1b..3069e4f376a 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -24,7 +24,11 @@ import { ThinkingBudget } from "../ThinkingBudget" type OpenAICompatibleProps = { apiConfiguration: ProviderSettings - setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + setApiConfigurationField: ( + field: K, + value: ProviderSettings[K], + isUserAction?: boolean, + ) => void organizationAllowList: OrganizationAllowList modelValidationError?: string simplifySettings?: boolean @@ -88,7 +92,8 @@ export const OpenAICompatible = ({ useEffect(() => { const timer = setTimeout(() => { const headerObject = convertHeadersToObject(customHeaders) - setApiConfigurationField("openAiHeaders", headerObject) + // Pass false to indicate this is automatic sync, not a user action + setApiConfigurationField("openAiHeaders", headerObject, false) }, 300) return () => clearTimeout(timer)