diff --git a/static/boot.js b/static/boot.js index 67aa84c4..d041548c 100644 --- a/static/boot.js +++ b/static/boot.js @@ -1576,20 +1576,30 @@ function applyBotName(){ // options are enough for first paint; the dynamic provider list can settle // after the saved session is visible. const _hydrateBootModelDropdown=()=>populateModelDropdown().then(()=>{ + const sessionModelState=S.session&&S.session.model + ? {model:S.session.model,model_provider:S.session.model_provider||null} + : null; const savedState=(typeof _readPersistedModelState==='function') ? _readPersistedModelState() : (localStorage.getItem('hermes-webui-model')?{model:localStorage.getItem('hermes-webui-model'),model_provider:null}:null); - const savedModel=savedState&&savedState.model; - // Guardrail: prefer profile/server default on boot when available. - // Persisted browser model state should not override a valid default model. - const allowBootSavedModelOverride=!window._defaultModel; - if(savedModel && $('modelSelect') && allowBootSavedModelOverride){ + // Active sessions are authoritative. On fresh boot without a restored + // session, keep the profile/server default ahead of stale browser model + // state when a default exists. + const stateToApply=sessionModelState||(!window._defaultModel?savedState:null); + const savedModel=stateToApply&&stateToApply.model; + if(savedModel && $('modelSelect')){ const applied=(typeof _applyModelToDropdown==='function') - ? _applyModelToDropdown(savedModel,$('modelSelect'),savedState.model_provider||null) + ? (sessionModelState + ? _applyModelToDropdown(sessionModelState.model,$('modelSelect'),sessionModelState.model_provider||null) + : _applyModelToDropdown(savedState.model,$('modelSelect'),savedState.model_provider||null)) : null; - if(!applied) $('modelSelect').value=savedModel; - // If the value didn't take (model not in list), clear the bad pref - if(!applied&&$('modelSelect').value!==savedModel){ + if(!applied) $('modelSelect').value=stateToApply.model; + // If the value didn't take (model not in list), clear the bad pref only + // for persisted browser preferences. Active sessions remain authoritative. + if(!applied&&sessionModelState&&typeof _ensureModelOptionInDropdown==='function'){ + _ensureModelOptionInDropdown(sessionModelState.model,$('modelSelect'),sessionModelState.model_provider||null); + } + else if(!applied&&!sessionModelState&&$('modelSelect').value!==stateToApply.model){ if(typeof _clearPersistedModelState==='function') _clearPersistedModelState(); else { localStorage.removeItem('hermes-webui-model'); diff --git a/static/sessions.js b/static/sessions.js index 08a1e381..d828862b 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -490,8 +490,17 @@ async function newSession(flash, options={}){ const sessionProvider=S.session.model_provider||null; const currentProvider=currentModelState.model_provider||null; if(S.session.model!==modelSel.value || sessionProvider !== currentProvider){ - _applyModelToDropdown(S.session.model,modelSel,sessionProvider); - if(typeof syncModelChip==='function') syncModelChip(); + let sessionModelApplied=_applyModelToDropdown(S.session.model,modelSel,sessionProvider); + if(!sessionModelApplied){ + const opt=document.createElement('option'); + opt.value=S.session.model; + opt.textContent=typeof getModelLabel==='function'?getModelLabel(S.session.model):S.session.model; + opt.dataset.custom='1'; + opt.dataset.provider=sessionProvider||''; + modelSel.appendChild(opt); + sessionModelApplied=_applyModelToDropdown(S.session.model,modelSel,sessionProvider); + } + if(sessionModelApplied&&typeof syncModelChip==='function') syncModelChip(); } } // Reset per-session visual state: a fresh chat is idle even if another diff --git a/tests/test_new_chat_default_model_frontend.py b/tests/test_new_chat_default_model_frontend.py index e12399bc..121df78f 100644 --- a/tests/test_new_chat_default_model_frontend.py +++ b/tests/test_new_chat_default_model_frontend.py @@ -35,6 +35,27 @@ def test_new_chat_syncs_model_picker_when_default_provider_changes_but_model_id_ assert "_applyModelToDropdown(S.session.model,modelSel,sessionProvider)" in fn +def test_new_chat_inserts_session_model_when_static_picker_lacks_default(): + fn = _new_session_function() + assert "sessionModelApplied" in fn + assert "document.createElement('option')" in fn + assert "opt.value=S.session.model" in fn + assert "opt.dataset.provider=sessionProvider||''" in fn + assert "modelSel.appendChild(opt)" in fn + + +def test_boot_model_hydration_prefers_active_session_over_persisted_model(): + boot_js = Path("static/boot.js").read_text(encoding="utf-8") + marker = "const sessionModelState=S.session&&S.session.model" + assert marker in boot_js + session_branch = boot_js[boot_js.index(marker) : boot_js.index("if(S.session) syncTopbar();", boot_js.index(marker))] + assert "_applyModelToDropdown(sessionModelState.model,$('modelSelect'),sessionModelState.model_provider||null)" in session_branch + assert "savedState" in session_branch + assert session_branch.index("sessionModelState") < session_branch.index("savedState"), ( + "active session model must be considered before localStorage so stale saved model preferences cannot override new chats" + ) + + def test_new_chat_does_not_send_stale_dropdown_model_when_session_has_default_model(): assert "model:S.session.model||$('modelSelect').value" in MESSAGES_JS assert "model_provider:S.session.model_provider||null" in MESSAGES_JS