feat(#5497): reuse the rich model picker in Preferences
This commit is contained in:
1 parent
5e0d4d1e3d
commit
087c694c8d
7 files changed
+290
-20
No files matched your search
@@ -1136,8 +1136,10 @@
|
||||
<div class="settings-field">
|
||||
<label for="settingsModel" data-i18n="settings_label_model">Default Model</label>
|
||||
<div class="model-advanced-row">
|
||||
<button type="button" id="settingsModelChip" class="settings-model-chip" aria-haspopup="listbox" aria-expanded="false">Default Model</button>
|
||||
<select id="settingsModel" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px"></select>
|
||||
<button type="button" id="mainAdvancedBtn" class="model-advanced-btn" title="Advanced options" aria-label="Advanced options for main model"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
|
||||
<div class="model-dropdown settings-model-dropdown" id="settingsModelDropdown"></div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_model">Used for new conversations. Existing conversations keep their selected model.</div>
|
||||
</div>
|
||||
|
||||
@@ -8644,7 +8644,13 @@ async function loadSettingsPanel(){
|
||||
}else{
|
||||
modelSel.value=_settingsHermesDefaultModelOnOpen;
|
||||
}
|
||||
if(typeof closeSettingsModelDropdown==='function') closeSettingsModelDropdown();
|
||||
if(typeof mountSettingsModelPicker==='function') mountSettingsModelPicker();
|
||||
modelSel.addEventListener('change',_markSettingsDirty,{once:false});
|
||||
if(!modelSel._settingsChipSyncBound){
|
||||
modelSel._settingsChipSyncBound=true;
|
||||
modelSel.addEventListener('change',()=>{if(typeof syncSettingsModelChip==='function') syncSettingsModelChip();},{once:false});
|
||||
}
|
||||
}
|
||||
// Auxiliary models — load task assignments and provider/model options
|
||||
_bindMainAdvancedOptionsButton();
|
||||
|
||||
@@ -4840,16 +4840,55 @@ main.main > #mainPlugin{display:none;}
|
||||
#mainSettings .settings-field > div[style*="color: var(--muted)"]{font-size:12px;color:var(--muted);line-height:1.5;}
|
||||
|
||||
.model-advanced-row{
|
||||
position:relative;
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) 34px;
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
}
|
||||
.model-advanced-row #settingsModel{
|
||||
display:none;
|
||||
width:100%!important;
|
||||
min-width:0;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.settings-model-chip{
|
||||
width:100%;
|
||||
min-width:0;
|
||||
box-sizing:border-box;
|
||||
padding:8px 10px;
|
||||
border:1px solid var(--border2);
|
||||
border-radius:6px;
|
||||
background:var(--code-bg);
|
||||
color:var(--text);
|
||||
font:inherit;
|
||||
font-size:13px;
|
||||
text-align:left;
|
||||
cursor:pointer;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.settings-model-chip:hover,.settings-model-chip.active,.settings-model-chip:focus-visible{
|
||||
border-color:var(--accent);
|
||||
outline:none;
|
||||
}
|
||||
.settings-model-dropdown{
|
||||
top:calc(100% + 4px);
|
||||
bottom:auto;
|
||||
left:0;
|
||||
width:calc(100% - 42px);
|
||||
min-width:min(360px,100%);
|
||||
max-width:100%;
|
||||
max-height:360px;
|
||||
z-index:260;
|
||||
}
|
||||
.settings-model-dropdown .model-search-row{
|
||||
position:sticky;
|
||||
top:0;
|
||||
background:var(--surface);
|
||||
z-index:1;
|
||||
}
|
||||
.model-advanced-btn{
|
||||
width:32px;
|
||||
height:32px;
|
||||
|
||||
+135
-16
@@ -2430,23 +2430,36 @@ function _refreshOpenModelDropdown(){
|
||||
renderModelDropdown();
|
||||
if(typeof _positionModelDropdown==='function') _positionModelDropdown();
|
||||
}
|
||||
const sdd=$('settingsModelDropdown');
|
||||
if(sdd&&sdd.classList&&sdd.classList.contains('open')&&typeof renderModelDropdown==='function'){
|
||||
renderModelDropdown({
|
||||
dropdownId:'settingsModelDropdown',
|
||||
selectId:'settingsModel',
|
||||
forceOpenKey:'settingsModel',
|
||||
closeDropdown:closeSettingsModelDropdown,
|
||||
selectModel:selectSettingsModelFromDropdown,
|
||||
scopeNoteText:t('settings_desc_model')||'Used for new conversations. Existing conversations keep their selected model.',
|
||||
});
|
||||
}
|
||||
}
|
||||
function _applyModelToDropdown(modelId, sel, preferredProviderId, opts){
|
||||
if(!modelId||!sel) return null;
|
||||
const currentState=(sel.id==='modelSelect'&&typeof _modelStateForSelect==='function')
|
||||
const isRichPickerSelect=sel.id==='modelSelect'||sel.id==='settingsModel';
|
||||
const currentState=(isRichPickerSelect&&typeof _modelStateForSelect==='function')
|
||||
? _modelStateForSelect(sel, sel.value)
|
||||
: null;
|
||||
const resolved=_findModelInDropdown(modelId,sel,preferredProviderId);
|
||||
if(resolved){
|
||||
sel.value=resolved;
|
||||
if(sel.id==='modelSelect'){
|
||||
if(isRichPickerSelect){
|
||||
const resolvedState=typeof _modelStateForSelect==='function'
|
||||
? _modelStateForSelect(sel, resolved)
|
||||
: {model:resolved,model_provider:preferredProviderId||null};
|
||||
const pickerChanged= !!(opts&&opts.forceRefresh) || !currentState
|
||||
|| String(currentState.model||'')!==String(resolvedState.model||'')
|
||||
|| String(currentState.model_provider||'')!==String(resolvedState.model_provider||'');
|
||||
if(typeof syncModelChip==='function') syncModelChip();
|
||||
if(sel.id==='modelSelect'&&typeof syncModelChip==='function') syncModelChip();
|
||||
if(sel.id==='settingsModel'&&typeof syncSettingsModelChip==='function') syncSettingsModelChip();
|
||||
if(pickerChanged) _refreshOpenModelDropdown();
|
||||
}
|
||||
return resolved;
|
||||
@@ -2472,6 +2485,10 @@ function _ensureModelOptionInDropdown(modelId, sel, preferredProviderId){
|
||||
if(typeof syncModelChip==='function') syncModelChip();
|
||||
_refreshOpenModelDropdown();
|
||||
}
|
||||
if(sel.id==='settingsModel'){
|
||||
if(typeof syncSettingsModelChip==='function') syncSettingsModelChip();
|
||||
_refreshOpenModelDropdown();
|
||||
}
|
||||
return modelId;
|
||||
}
|
||||
function _modelStateFromAppliedDropdown(sel, modelValue){
|
||||
@@ -3078,9 +3095,16 @@ function _mountSearchableModelSelect(opts={}){
|
||||
}
|
||||
|
||||
function renderModelDropdown(){
|
||||
const dd=$('composerModelDropdown');
|
||||
const sel=$('modelSelect');
|
||||
const opts=arguments[0]||{};
|
||||
const dd=$(opts.dropdownId||'composerModelDropdown');
|
||||
const sel=$(opts.selectId||'modelSelect');
|
||||
if(!dd||!sel) return;
|
||||
const selectFromDropdown=typeof opts.selectModel==='function'
|
||||
? opts.selectModel
|
||||
: (value,provider)=>selectModelFromDropdown(value,provider);
|
||||
const closeDropdown=typeof opts.closeDropdown==='function'
|
||||
? opts.closeDropdown
|
||||
: closeModelDropdown;
|
||||
// Group(s) that must render OPEN even though they aren't the selected group —
|
||||
// set when the user expands a group's overflow via "Show more" so a later full
|
||||
// re-render doesn't re-collapse it (_groupOpenState is rebuilt per render, so
|
||||
@@ -3089,8 +3113,10 @@ function renderModelDropdown(){
|
||||
// #3691 node test driver evals the function body without module scope).
|
||||
const _forceOpenGroups=(()=>{
|
||||
const _g=(typeof window!=='undefined')?window:(typeof globalThis!=='undefined'?globalThis:{});
|
||||
if(!_g.__modelGroupForceOpen) _g.__modelGroupForceOpen=new Set();
|
||||
return _g.__modelGroupForceOpen;
|
||||
const key=opts.forceOpenKey||'composer';
|
||||
if(!_g.__modelGroupForceOpenByPicker) _g.__modelGroupForceOpenByPicker={};
|
||||
if(!_g.__modelGroupForceOpenByPicker[key]) _g.__modelGroupForceOpenByPicker[key]=new Set();
|
||||
return _g.__modelGroupForceOpenByPicker[key];
|
||||
})();
|
||||
const _modelData=[];
|
||||
const _groupMeta=new Map();
|
||||
@@ -3187,7 +3213,7 @@ function renderModelDropdown(){
|
||||
// Create search input FIRST before filterModels definition
|
||||
const _scopeNote=document.createElement('div');
|
||||
_scopeNote.className='model-scope-note';
|
||||
_scopeNote.textContent=t('model_scope_advisory')||'Applies to this conversation from your next message.';
|
||||
_scopeNote.textContent=opts.scopeNoteText||(t('model_scope_advisory')||'Applies to this conversation from your next message.');
|
||||
const _searchRow=document.createElement('div');
|
||||
_searchRow.className='model-search-row';
|
||||
_searchRow.innerHTML=`<input class="model-search-input" type="text" placeholder="${esc(t('model_search_placeholder')||'Search models…')}" spellcheck="false" autocomplete="off"><button class="model-search-clear" title="Clear search">${li('x',10)}</button>`;
|
||||
@@ -3227,7 +3253,7 @@ function renderModelDropdown(){
|
||||
const _plainGroup=m.group?String(m.group).replace(/\s*\(\d+\s+of\s+\d+\)\s*$/,''):'';
|
||||
const providerChip=(_plainGroup&&withProviderChip)?`<span class="model-opt-provider">${esc(_plainGroup)}</span>`:'';
|
||||
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
|
||||
row.onclick=()=>selectModelFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
|
||||
row.onclick=()=>selectFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
|
||||
return row;
|
||||
};
|
||||
const _expandOverflowGroup=(groupMetaEntry)=>{
|
||||
@@ -3251,7 +3277,7 @@ function renderModelDropdown(){
|
||||
// expander gone, search term reapplied.
|
||||
const _fullReRender=()=>{
|
||||
const _term=(_si&&_si.value)||'';
|
||||
renderModelDropdown();
|
||||
renderModelDropdown(opts);
|
||||
const ns=dd.querySelector('.model-search-input');
|
||||
if(ns){ ns.value=_term; (ns._listeners&&ns._listeners.input)?ns._listeners.input():ns.dispatchEvent(new Event('input')); }
|
||||
};
|
||||
@@ -3280,7 +3306,7 @@ function renderModelDropdown(){
|
||||
for(const m of extraModels){
|
||||
if(!m||!m.id) continue;
|
||||
if(_alreadyShown.has(esc(m.id))) continue;
|
||||
const row=_buildModelRow({value:m.id,name:m.label||m.id,id:m.id,group:_plainLabel,groupKey,providerId:(og.dataset&&og.dataset.provider)||''},$('modelSelect'),false);
|
||||
const row=_buildModelRow({value:m.id,name:m.label||m.id,id:m.id,group:_plainLabel,groupKey,providerId:(og.dataset&&og.dataset.provider)||''},sel,false);
|
||||
wrap.insertBefore(row,moreEl);
|
||||
if(!firstNewRow) firstNewRow=row;
|
||||
}
|
||||
@@ -3346,7 +3372,7 @@ function renderModelDropdown(){
|
||||
const _underOwnHeading=shouldRenderHeading&&!!(m.groupKey&&_groupWrappers[m.groupKey]);
|
||||
const providerChip=(_plainGroup&&!_underOwnHeading)?`<span class="model-opt-provider">${esc(_plainGroup)}</span>`:'';
|
||||
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
|
||||
row.onclick=()=>selectModelFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
|
||||
row.onclick=()=>selectFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
|
||||
return row;
|
||||
};
|
||||
const _filterModels=(term)=>{
|
||||
@@ -3441,7 +3467,7 @@ function renderModelDropdown(){
|
||||
}
|
||||
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(badgeLabel)}</span>`:'';
|
||||
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(modelName)}</span>${badgeHtml}</div><span class="model-opt-id">${esc(m.id)}</span>`;
|
||||
row.onclick=()=>selectModelFromDropdown(m.value,(m.badge&&m.badge.provider)||m.providerId||null);
|
||||
row.onclick=()=>selectFromDropdown(m.value,(m.badge&&m.badge.provider)||m.providerId||null);
|
||||
dd.appendChild(row);
|
||||
}
|
||||
}
|
||||
@@ -3609,7 +3635,7 @@ function renderModelDropdown(){
|
||||
if(typeof row.scrollIntoView==='function') row.scrollIntoView({block:'nearest'});
|
||||
};
|
||||
_si.addEventListener('keydown',e=>{
|
||||
if(e.key==='Escape'){closeModelDropdown();return;}
|
||||
if(e.key==='Escape'){closeDropdown();return;}
|
||||
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){
|
||||
const rows=_visibleModelRows();
|
||||
if(!rows.length){if(e.key==='Enter') e.preventDefault();return;}
|
||||
@@ -3626,9 +3652,9 @@ function renderModelDropdown(){
|
||||
_si.addEventListener('click',e=>e.stopPropagation());
|
||||
_sc.onclick=()=>{ _si.value=''; _filterModels(''); _si.focus(); };
|
||||
_sc.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){ _si.value=''; _filterModels(''); _si.focus(); e.preventDefault(); }});
|
||||
const _applyCustom=()=>{const v=_ci.value.trim();if(!v)return;selectModelFromDropdown(v);_ci.value='';};
|
||||
const _applyCustom=()=>{const v=_ci.value.trim();if(!v)return;selectFromDropdown(v,null);_ci.value='';};
|
||||
_cb.onclick=_applyCustom;
|
||||
_ci.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();_applyCustom();}if(e.key==='Escape'){closeModelDropdown();}});
|
||||
_ci.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();_applyCustom();}if(e.key==='Escape'){closeDropdown();}});
|
||||
_ci.addEventListener('click',e=>e.stopPropagation());
|
||||
dd.appendChild(_scopeNote);
|
||||
dd.appendChild(_searchRow);
|
||||
@@ -3693,12 +3719,105 @@ function closeModelDropdown(){
|
||||
if(mobileAction) mobileAction.classList.remove('active');
|
||||
}
|
||||
|
||||
function closeSettingsModelDropdown(){
|
||||
const dd=$('settingsModelDropdown');
|
||||
const chip=$('settingsModelChip');
|
||||
if(dd) dd.classList.remove('open');
|
||||
if(chip){
|
||||
chip.classList.remove('active');
|
||||
chip.setAttribute('aria-expanded','false');
|
||||
}
|
||||
}
|
||||
|
||||
function syncSettingsModelChip(){
|
||||
const sel=$('settingsModel');
|
||||
const chip=$('settingsModelChip');
|
||||
if(!sel||!chip) return;
|
||||
const opt=sel.selectedOptions&&sel.selectedOptions[0];
|
||||
const text=(opt&&opt.textContent)||getModelLabel(sel.value||'')||t('settings_label_model')||'Default Model';
|
||||
chip.textContent=text;
|
||||
chip.title=sel.value||text;
|
||||
}
|
||||
|
||||
function selectSettingsModelFromDropdown(value,preferredProviderId){
|
||||
const sel=$('settingsModel');
|
||||
if(!sel){closeSettingsModelDropdown();return;}
|
||||
const provider=String(preferredProviderId||'').trim()||null;
|
||||
if(typeof _ensureModelOptionInDropdown==='function'){
|
||||
_ensureModelOptionInDropdown(value,sel,provider);
|
||||
}else{
|
||||
sel.value=value;
|
||||
if(typeof syncSettingsModelChip==='function') syncSettingsModelChip();
|
||||
}
|
||||
closeSettingsModelDropdown();
|
||||
try{
|
||||
if(typeof Event==='function') sel.dispatchEvent(new Event('change',{bubbles:true}));
|
||||
else if(typeof sel.onchange==='function') sel.onchange();
|
||||
}catch(_){}
|
||||
}
|
||||
|
||||
function openSettingsModelDropdown(){
|
||||
const dd=$('settingsModelDropdown');
|
||||
const sel=$('settingsModel');
|
||||
const chip=$('settingsModelChip');
|
||||
if(!dd||!sel) return;
|
||||
renderModelDropdown({
|
||||
dropdownId:'settingsModelDropdown',
|
||||
selectId:'settingsModel',
|
||||
forceOpenKey:'settingsModel',
|
||||
closeDropdown:closeSettingsModelDropdown,
|
||||
selectModel:selectSettingsModelFromDropdown,
|
||||
scopeNoteText:t('settings_desc_model')||'Used for new conversations. Existing conversations keep their selected model.',
|
||||
});
|
||||
dd.classList.add('open');
|
||||
if(chip){
|
||||
chip.classList.add('active');
|
||||
chip.setAttribute('aria-expanded','true');
|
||||
}
|
||||
setTimeout(()=>{
|
||||
const input=dd.querySelector('.model-search-input');
|
||||
if(input) input.focus();
|
||||
},0);
|
||||
}
|
||||
|
||||
function toggleSettingsModelDropdown(){
|
||||
const dd=$('settingsModelDropdown');
|
||||
if(dd&&dd.classList.contains('open')){closeSettingsModelDropdown();return;}
|
||||
openSettingsModelDropdown();
|
||||
}
|
||||
|
||||
function mountSettingsModelPicker(){
|
||||
const chip=$('settingsModelChip');
|
||||
const sel=$('settingsModel');
|
||||
if(!chip||!sel) return;
|
||||
syncSettingsModelChip();
|
||||
if(!chip._settingsModelPickerBound){
|
||||
chip._settingsModelPickerBound=true;
|
||||
chip.addEventListener('click',e=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleSettingsModelDropdown();
|
||||
});
|
||||
chip.addEventListener('keydown',e=>{
|
||||
if(e.key==='Enter'||e.key===' '||e.key==='ArrowDown'){
|
||||
e.preventDefault();
|
||||
toggleSettingsModelDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click',e=>{
|
||||
if(
|
||||
!e.target.closest('#composerModelChip') &&
|
||||
!e.target.closest('#composerMobileModelAction') &&
|
||||
!e.target.closest('#composerModelDropdown')
|
||||
) closeModelDropdown();
|
||||
if(
|
||||
!e.target.closest('#settingsModelChip') &&
|
||||
!e.target.closest('#settingsModel') &&
|
||||
!e.target.closest('#settingsModelDropdown')
|
||||
) closeSettingsModelDropdown();
|
||||
});
|
||||
window.addEventListener('resize',()=>{
|
||||
const dd=$('composerModelDropdown');
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_highlight_class_has_style():
|
||||
|
||||
def test_escape_still_closes_dropdown():
|
||||
# Existing Escape-to-close behavior must not regress.
|
||||
assert "if(e.key==='Escape'){closeModelDropdown();return;}" in UI_JS
|
||||
assert "if(e.key==='Escape'){closeDropdown();return;}" in UI_JS
|
||||
|
||||
|
||||
def test_enter_picks_highlighted_row():
|
||||
|
||||
@@ -135,10 +135,13 @@ def test_selectModelFromDropdown_defers_reasoning_sync_to_onchange():
|
||||
|
||||
|
||||
def test_model_dropdown_passes_provider_to_select():
|
||||
"""Composer model rows must pass provider context into selectModelFromDropdown."""
|
||||
"""Composer model rows must pass provider context through the shared picker callback."""
|
||||
with open("static/ui.js") as f:
|
||||
src = f.read()
|
||||
assert "selectModelFromDropdown(value,provider)" in src, (
|
||||
"composer default picker callback must still route to selectModelFromDropdown"
|
||||
)
|
||||
assert re.search(
|
||||
r"selectModelFromDropdown\(m\.value,\s*m\.providerId",
|
||||
r"selectFromDropdown\(m\.value,\s*m\.providerId",
|
||||
src,
|
||||
), "model dropdown rows must pass providerId to selectModelFromDropdown"
|
||||
), "model dropdown rows must pass providerId through the shared callback"
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Regression coverage for #5497 Preferences default model picker parity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
|
||||
PANELS_JS = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
||||
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(src: str, name: str) -> str:
|
||||
match = re.search(rf"function\s+{re.escape(name)}\s*\([^)]*\)\s*\{{", src)
|
||||
assert match, f"{name} not found"
|
||||
start = match.end()
|
||||
depth = 1
|
||||
pos = start
|
||||
while pos < len(src) and depth:
|
||||
if src[pos] == "{":
|
||||
depth += 1
|
||||
elif src[pos] == "}":
|
||||
depth -= 1
|
||||
pos += 1
|
||||
assert depth == 0, f"{name} body did not terminate"
|
||||
return src[start : pos - 1]
|
||||
|
||||
|
||||
def test_preferences_default_model_field_keeps_source_select_and_adds_rich_picker_shell():
|
||||
field_start = INDEX_HTML.index('id="settingsModelChip"')
|
||||
field_end = INDEX_HTML.index('data-i18n="settings_desc_model"', field_start)
|
||||
field = INDEX_HTML[field_start:field_end]
|
||||
|
||||
assert 'id="settingsModelChip"' in field
|
||||
assert 'id="settingsModel"' in field
|
||||
assert 'id="settingsModelDropdown"' in field
|
||||
assert 'class="model-dropdown settings-model-dropdown"' in field
|
||||
assert 'id="mainAdvancedBtn"' in field
|
||||
assert field.index('id="settingsModelChip"') < field.index('id="mainAdvancedBtn"')
|
||||
|
||||
|
||||
def test_settings_model_select_is_hidden_but_remains_advanced_options_source():
|
||||
assert ".model-advanced-row #settingsModel" in STYLE_CSS
|
||||
assert "display:none;" in STYLE_CSS[STYLE_CSS.index(".model-advanced-row #settingsModel") : STYLE_CSS.index(".model-advanced-btn")]
|
||||
|
||||
bind_body = _function_body(PANELS_JS, "_bindMainAdvancedOptionsButton")
|
||||
assert "const modelSel=$('settingsModel');" in bind_body
|
||||
assert "modelSel.parentElement" in bind_body
|
||||
assert "mainAdvancedBtn" in bind_body
|
||||
|
||||
|
||||
def test_render_model_dropdown_accepts_settings_select_and_callbacks():
|
||||
body = _function_body(UI_JS, "renderModelDropdown")
|
||||
|
||||
assert "opts.dropdownId||'composerModelDropdown'" in body
|
||||
assert "opts.selectId||'modelSelect'" in body
|
||||
assert "opts.selectModel" in body
|
||||
assert "opts.closeDropdown" in body
|
||||
assert "selectFromDropdown(m.value" in body
|
||||
assert "row.onclick=()=>selectModelFromDropdown" not in body
|
||||
assert "closeDropdown();return;" in body
|
||||
assert "opts.forceOpenKey||'composer'" in body
|
||||
assert "renderModelDropdown(opts)" in body
|
||||
assert "opts.scopeNoteText||" in body
|
||||
|
||||
|
||||
def test_preferences_picker_routes_through_shared_renderer_without_panel_copy():
|
||||
mount_body = _function_body(UI_JS, "openSettingsModelDropdown")
|
||||
select_body = _function_body(UI_JS, "selectSettingsModelFromDropdown")
|
||||
panels_model_block = PANELS_JS[
|
||||
PANELS_JS.index("const modelSel=$('settingsModel');") : PANELS_JS.index("// Auxiliary models", PANELS_JS.index("const modelSel=$('settingsModel');"))
|
||||
]
|
||||
|
||||
assert "renderModelDropdown({" in mount_body
|
||||
assert "dropdownId:'settingsModelDropdown'" in mount_body
|
||||
assert "selectId:'settingsModel'" in mount_body
|
||||
assert "selectModel:selectSettingsModelFromDropdown" in mount_body
|
||||
assert "scopeNoteText:t('settings_desc_model')" in mount_body
|
||||
assert "_ensureModelOptionInDropdown(value,sel,provider)" in select_body
|
||||
assert "sel.dispatchEvent(new Event('change',{bubbles:true}))" in select_body
|
||||
assert "syncSettingsModelChip" in select_body
|
||||
assert "closeSettingsModelDropdown" in panels_model_block
|
||||
assert "mountSettingsModelPicker" in panels_model_block
|
||||
assert "_settingsChipSyncBound" in panels_model_block
|
||||
assert ".model-opt" not in PANELS_JS
|
||||
assert "model-search-input" not in PANELS_JS
|
||||
|
||||
|
||||
def test_preferences_picker_refreshes_existing_save_and_dirty_contracts():
|
||||
autosave_body = _function_body(PANELS_JS, "_autosavePreferencesSettings")
|
||||
save_body = _function_body(PANELS_JS, "saveSettings")
|
||||
refresh_body = _function_body(UI_JS, "_refreshOpenModelDropdown")
|
||||
|
||||
assert "_captureModelDropdownSelection(modelSel)" in autosave_body
|
||||
assert "_captureModelDropdownSelection($('settingsModel'))" in save_body
|
||||
assert "await api('/api/default-model',{method:'POST',body:JSON.stringify({model,provider:modelState.model_provider||null})});" in save_body
|
||||
assert "settingsModelDropdown" in refresh_body
|
||||
assert "selectModel:selectSettingsModelFromDropdown" in refresh_body
|
||||
Reference in new issue
Block a user