From 6433d6577353939b9552b1f75b557b4bcdd6b5ed Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 9 Feb 2026 12:16:09 -0500 Subject: [PATCH] fix: prevent false unsaved changes prompt with OpenAI Compatible headers Mark automatic header syncs in ApiOptions and OpenAICompatible as non-user actions (isUserAction: false) and enhance SettingsView change detection to skip automatic syncs with semantically equal values. Root cause: two components (ApiOptions and OpenAICompatible) manage openAiHeaders state and automatically sync it back on mount/remount. These syncs were treated as user changes, triggering a false dirty state. Co-authored-by: Robert McIntyre --- .../src/components/settings/ApiOptions.tsx | 2 +- .../src/components/settings/SettingsView.tsx | 17 +++++++++++++++-- .../settings/providers/OpenAICompatible.tsx | 8 ++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 5087b0123ea..8aa14e2dc97 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -155,7 +155,7 @@ const ApiOptions = ({ // Only update if the processed object is different from the current config. if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) { - setApiConfigurationField("openAiHeaders", newHeadersObject) + setApiConfigurationField("openAiHeaders", newHeadersObject, false) } }, 300, diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 86f4ded3382..1d35da68a3f 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -262,9 +262,19 @@ 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 => { + if (a === b) return true + if (a == null && b == null) return true + if (typeof a !== typeof b) return false + 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) && @@ -272,7 +282,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..0524932c5fa 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,7 @@ export const OpenAICompatible = ({ useEffect(() => { const timer = setTimeout(() => { const headerObject = convertHeadersToObject(customHeaders) - setApiConfigurationField("openAiHeaders", headerObject) + setApiConfigurationField("openAiHeaders", headerObject, false) }, 300) return () => clearTimeout(timer)