diff --git a/static/panels.js b/static/panels.js index 7a4fb89f..2a1bea70 100644 --- a/static/panels.js +++ b/static/panels.js @@ -2748,9 +2748,22 @@ async function _autosavePreferencesSettings(payload){ await api('/api/settings',{method:'POST',body:JSON.stringify(payload)}); _settingsPreferencesAutosaveRetryPayload=null; _setPreferencesAutosaveStatus('saved'); - _settingsDirty=false; - const bar=$('settingsUnsavedBar'); - if(bar) bar.style.display='none'; + // Only clear the global dirty flag and hide the unsaved-changes bar when + // there is no pending edit on a manually-saved field. Password and model + // are still committed via the explicit "Save Settings" button (password + // for security; model goes through /api/default-model). Without this + // guard, autosaving a checkbox right after a user typed in the password + // field would silently dismiss the password edit. (Opus pre-release + // review of v0.50.250, SHOULD-FIX Q1.) + const pwField=$('settingsPassword'); + const pwDirty=!!(pwField&&pwField.value); + const modelSel=$('settingsModel'); + const modelDirty=!!(modelSel&&((modelSel.value||'')!==(_settingsHermesDefaultModelOnOpen||''))); + if(!pwDirty&&!modelDirty){ + _settingsDirty=false; + const bar=$('settingsUnsavedBar'); + if(bar) bar.style.display='none'; + } }catch(e){ console.warn('[settings] preferences autosave failed', e); _setPreferencesAutosaveStatus('failed'); diff --git a/tests/test_1003_preferences_autosave.py b/tests/test_1003_preferences_autosave.py index 39a16c5c..5283a5c0 100644 --- a/tests/test_1003_preferences_autosave.py +++ b/tests/test_1003_preferences_autosave.py @@ -105,12 +105,29 @@ def test_password_still_uses_mark_dirty(): def test_autosave_clears_dirty_flag_and_hides_unsaved_bar(): """_autosavePreferencesSettings must clear the dirty flag and hide the - unsaved-changes bar on success — otherwise the bar shows stale state.""" + unsaved-changes bar on success — but ONLY when password and model are + not pending. Q1 from Opus pre-release review of v0.50.250.""" block = _function_block(PANELS_JS, "_autosavePreferencesSettings") - assert "_settingsDirty=false" in block.replace(" ", ""), \ - "_autosavePreferencesSettings must set _settingsDirty=false on success" - assert "settingsUnsavedBar" in block, \ - "_autosavePreferencesSettings must hide settingsUnsavedBar on success" + # Must check pwField/modelSel state before clearing dirty + hiding bar + assert "settingsPassword" in block, ( + "_autosavePreferencesSettings must check the password field before " + "clearing _settingsDirty (Opus SHOULD-FIX Q1: autosave was clobbering " + "pending password edits)" + ) + assert "settingsModel" in block, ( + "_autosavePreferencesSettings must check the model selector before " + "clearing _settingsDirty (autosave was clobbering pending model changes)" + ) + assert "_settingsHermesDefaultModelOnOpen" in block, ( + "_autosavePreferencesSettings must compare the model selector value " + "against the on-open snapshot to detect a pending change" + ) + # The clear-and-hide block must be conditional, not unconditional + compact = block.replace(" ", "").replace("\n", "") + assert "if(!pwDirty&&!modelDirty)" in compact or "if(pwDirty||modelDirty)" in compact, ( + "_autosavePreferencesSettings must guard the dirty-clear and bar-hide " + "with a condition that defers when a manual field has pending edits" + ) def test_status_div_exists_in_index_html():