stage #5075
This commit is contained in:
commit
04191939d2
7 changed files with 404 additions and 42 deletions
|
|
@ -7847,6 +7847,7 @@ _SETTINGS_DEFAULTS = {
|
|||
"inflight_state_max_json_chars": 1500000, # max serialized recovery snapshot payload before pruning
|
||||
"hidden_tabs": [], # sidebar tab panel names hidden by user (e.g. ["tasks","kanban"]); chat and settings are always visible
|
||||
"tab_order": [], # user-defined sidebar/rail tab order for reorderable tabs; chat/settings stay fixed
|
||||
"composer_control_order": [], # user-defined composer footer control order; invalid/duplicate keys are ignored
|
||||
"language": "en", # UI locale code; must match a key in static/i18n.js LOCALES
|
||||
"bot_name": os.getenv(
|
||||
"HERMES_WEBUI_BOT_NAME", "Hermes"
|
||||
|
|
@ -7874,6 +7875,9 @@ _SETTINGS_LEGACY_DROP_KEYS = {
|
|||
"activity_feed_expanded_default",
|
||||
"simplified_tool_calling",
|
||||
}
|
||||
_COMPOSER_CONTROL_ORDER_KEYS = {
|
||||
key for key in _SETTINGS_DEFAULTS if key.startswith("hide_composer_")
|
||||
}
|
||||
_SETTINGS_THEME_VALUES = {"light", "dark", "system"}
|
||||
_SETTINGS_SKIN_VALUES = {
|
||||
"default",
|
||||
|
|
@ -8180,10 +8184,10 @@ def save_settings(settings: dict) -> dict:
|
|||
not isinstance(v, str) or not _SETTINGS_LANG_RE.match(v)
|
||||
):
|
||||
continue
|
||||
# Validate list-valued sidebar tab settings. Chat/settings stay fixed
|
||||
# even if a tampered POST tries to persist them, and duplicates are
|
||||
# collapsed while preserving the first requested order.
|
||||
if k in {"hidden_tabs", "tab_order"}:
|
||||
# Validate list-valued ordering settings. Chat/settings stay fixed
|
||||
# for tabs; composer ordering only accepts known control keys.
|
||||
# Duplicates are collapsed while preserving the first requested order.
|
||||
if k in {"hidden_tabs", "tab_order", "composer_control_order"}:
|
||||
if not isinstance(v, list):
|
||||
continue
|
||||
seen = set()
|
||||
|
|
@ -8192,7 +8196,11 @@ def save_settings(settings: dict) -> dict:
|
|||
if not isinstance(s, str):
|
||||
continue
|
||||
s = s.strip()
|
||||
if not s or s in {"chat", "settings"} or s in seen:
|
||||
if not s or s in seen:
|
||||
continue
|
||||
if k in {"hidden_tabs", "tab_order"} and s in {"chat", "settings"}:
|
||||
continue
|
||||
if k == "composer_control_order" and s not in _COMPOSER_CONTROL_ORDER_KEYS:
|
||||
continue
|
||||
seen.add(s)
|
||||
cleaned.append(s)
|
||||
|
|
|
|||
|
|
@ -2398,25 +2398,25 @@ function applyBotName(){
|
|||
}
|
||||
|
||||
const _COMPOSER_CONTROL_TOGGLE_DEFS=[
|
||||
{key:'hide_composer_attach',label:'Attach',labelKey:'composer_control_attach',selectors:['#btnAttach']},
|
||||
{key:'hide_composer_saved_prompts',label:'Saved prompts',labelKey:'composer_control_saved_prompts',selectors:['#btnSavedPrompts']},
|
||||
{key:'hide_composer_mic',label:'Mic',labelKey:'composer_control_mic',selectors:['#btnMic']},
|
||||
{key:'hide_composer_profile',label:'Profile',labelKey:'composer_control_profile',selectors:['#profileChipWrap']},
|
||||
{key:'hide_composer_workspace',label:'Workspace',labelKey:'composer_control_workspace',selectors:['.composer-ws-wrap','#composerMobileWorkspaceAction']},
|
||||
{key:'hide_composer_model',label:'Model',labelKey:'composer_control_model',selectors:['.composer-model-wrap','#composerMobileModelAction']},
|
||||
{key:'hide_composer_reasoning',label:'Reasoning',labelKey:'composer_control_reasoning',selectors:['#composerReasoningWrap','#composerMobileReasoningAction']},
|
||||
{key:'hide_composer_context',label:'Context',labelKey:'composer_control_context',selectors:['#ctxIndicatorWrap','#composerMobileContextAction']},
|
||||
{key:'hide_composer_attach',label:'Attach',labelKey:'composer_control_attach',selectors:['#btnAttach'],orderSelector:'#btnAttach',orderGroup:'left'},
|
||||
{key:'hide_composer_saved_prompts',label:'Saved prompts',labelKey:'composer_control_saved_prompts',selectors:['#btnSavedPrompts'],orderSelector:'#btnSavedPrompts',orderGroup:'left'},
|
||||
{key:'hide_composer_mic',label:'Mic',labelKey:'composer_control_mic',selectors:['#btnMic'],orderSelector:'#btnMic',orderGroup:'left'},
|
||||
{key:'hide_composer_profile',label:'Profile',labelKey:'composer_control_profile',selectors:['#profileChipWrap'],orderSelector:'#profileChipWrap',orderGroup:'left'},
|
||||
{key:'hide_composer_workspace',label:'Workspace',labelKey:'composer_control_workspace',selectors:['.composer-ws-wrap','#composerMobileWorkspaceAction'],orderSelector:'.composer-ws-wrap',orderGroup:'left'},
|
||||
{key:'hide_composer_model',label:'Model',labelKey:'composer_control_model',selectors:['.composer-model-wrap','#composerMobileModelAction'],orderSelector:'.composer-model-wrap',orderGroup:'left'},
|
||||
{key:'hide_composer_reasoning',label:'Reasoning',labelKey:'composer_control_reasoning',selectors:['#composerReasoningWrap','#composerMobileReasoningAction'],orderSelector:'#composerReasoningWrap',orderGroup:'left'},
|
||||
{key:'hide_composer_context',label:'Context',labelKey:'composer_control_context',selectors:['#ctxIndicatorWrap','#composerMobileContextAction'],orderSelector:'#ctxIndicatorWrap',orderGroup:'right'},
|
||||
];
|
||||
window._COMPOSER_CONTROL_TOGGLE_DEFS=_COMPOSER_CONTROL_TOGGLE_DEFS;
|
||||
|
||||
const _COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS=[
|
||||
{key:'hide_composer_voice_mode',label:'Voice mode',labelKey:'composer_control_voice_mode',selectors:['#btnVoiceMode']},
|
||||
{key:'hide_composer_yolo',label:'YOLO',labelKey:'composer_control_yolo',selectors:['#yoloPill']},
|
||||
{key:'hide_composer_bg_badge',label:'Background badge',labelKey:'composer_control_bg_badge',selectors:['#bgBadge']},
|
||||
{key:'hide_composer_mobile_config',label:'Mobile config',labelKey:'composer_control_mobile_config',selectors:['#composerMobileConfigBtn']},
|
||||
{key:'hide_composer_quota_chip',label:'Quota chip',labelKey:'composer_control_quota_chip',selectors:['#providerQuotaChip','#composerMobileQuotaAction']},
|
||||
{key:'hide_composer_toolsets',label:'Toolsets',labelKey:'composer_control_toolsets',selectors:['#composerToolsetsWrap']},
|
||||
{key:'hide_composer_status',label:'Status',labelKey:'composer_control_status',selectors:['#composerStatus']},
|
||||
{key:'hide_composer_voice_mode',label:'Voice mode',labelKey:'composer_control_voice_mode',selectors:['#btnVoiceMode'],orderSelector:'#btnVoiceMode',orderGroup:'left'},
|
||||
{key:'hide_composer_yolo',label:'YOLO',labelKey:'composer_control_yolo',selectors:['#yoloPill'],orderSelector:'#yoloPill',orderGroup:'left'},
|
||||
{key:'hide_composer_bg_badge',label:'Background badge',labelKey:'composer_control_bg_badge',selectors:['#bgBadge'],orderSelector:'#bgBadge',orderGroup:'right'},
|
||||
{key:'hide_composer_mobile_config',label:'Mobile config',labelKey:'composer_control_mobile_config',selectors:['#composerMobileConfigBtn'],orderSelector:'#composerMobileConfigBtn',orderGroup:'left'},
|
||||
{key:'hide_composer_quota_chip',label:'Quota chip',labelKey:'composer_control_quota_chip',selectors:['#providerQuotaChip','#composerMobileQuotaAction'],orderSelector:'#providerQuotaChip',orderGroup:'left'},
|
||||
{key:'hide_composer_toolsets',label:'Toolsets',labelKey:'composer_control_toolsets',selectors:['#composerToolsetsWrap'],orderSelector:'#composerToolsetsWrap',orderGroup:'left'},
|
||||
{key:'hide_composer_status',label:'Status',labelKey:'composer_control_status',selectors:['#composerStatus'],orderSelector:'#composerStatus',orderGroup:'right'},
|
||||
];
|
||||
window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS=_COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS;
|
||||
|
||||
|
|
@ -2424,6 +2424,57 @@ function _allComposerControlToggleDefs(){
|
|||
return _COMPOSER_CONTROL_TOGGLE_DEFS.concat(_COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS);
|
||||
}
|
||||
|
||||
function _sanitizeComposerControlOrder(order){
|
||||
if(!Array.isArray(order)) return [];
|
||||
const allowed=new Set(_allComposerControlToggleDefs().map(def=>def.key));
|
||||
const out=[];
|
||||
order.forEach(key=>{
|
||||
if(typeof key!=='string') return;
|
||||
key=key.trim();
|
||||
if(!key||!allowed.has(key)||out.includes(key)) return;
|
||||
out.push(key);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
window._sanitizeComposerControlOrder=_sanitizeComposerControlOrder;
|
||||
|
||||
function _orderedComposerControlDefs(order){
|
||||
const defs=_allComposerControlToggleDefs();
|
||||
const byKey=new Map(defs.map(def=>[def.key,def]));
|
||||
const out=[];
|
||||
_sanitizeComposerControlOrder(Array.isArray(order)?order:window._composerControlOrder).forEach(key=>{
|
||||
if(byKey.has(key)) out.push(byKey.get(key));
|
||||
});
|
||||
defs.forEach(def=>{if(!out.includes(def)) out.push(def);});
|
||||
return out;
|
||||
}
|
||||
window._orderedComposerControlDefs=_orderedComposerControlDefs;
|
||||
|
||||
function _applyComposerControlOrder(order){
|
||||
window._composerControlOrder=_sanitizeComposerControlOrder(order);
|
||||
const grouped=new Map();
|
||||
_orderedComposerControlDefs(window._composerControlOrder).forEach(def=>{
|
||||
const node=document.querySelector(def.orderSelector||def.selectors&&def.selectors[0]);
|
||||
if(!node||!node.parentNode) return;
|
||||
const parent=node.parentNode;
|
||||
if(!grouped.has(parent)) grouped.set(parent,[]);
|
||||
grouped.get(parent).push(node);
|
||||
});
|
||||
grouped.forEach((nodes,parent)=>{
|
||||
if(!nodes.length) return;
|
||||
const marker=document.createComment('composer-control-order');
|
||||
parent.insertBefore(marker,nodes[0]);
|
||||
let ref=marker;
|
||||
nodes.forEach(node=>{
|
||||
parent.insertBefore(node,ref.nextSibling);
|
||||
ref=node;
|
||||
});
|
||||
marker.remove();
|
||||
});
|
||||
if(typeof _fitComposerFooter==='function') _fitComposerFooter();
|
||||
}
|
||||
window._applyComposerControlOrder=_applyComposerControlOrder;
|
||||
|
||||
function _composerControlVisibilityFromSettings(settings){
|
||||
const next={};
|
||||
for(const def of _allComposerControlToggleDefs()){
|
||||
|
|
@ -2535,6 +2586,8 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
|||
window._largeTextPasteAsAttachment=s.large_text_paste_as_attachment!==false;
|
||||
window._projectQuickCreate=!!s.project_quick_create_buttons;
|
||||
window._composerControlVisibility=_composerControlVisibilityFromSettings(s);
|
||||
window._composerControlOrder=_sanitizeComposerControlOrder(s.composer_control_order);
|
||||
_applyComposerControlOrder(window._composerControlOrder);
|
||||
window._showTitlebarProfile=!!s.show_titlebar_profile;
|
||||
_applyTitlebarProfileVisibility();
|
||||
window._botName=s.bot_name||'Hermes';
|
||||
|
|
@ -2657,6 +2710,8 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
|||
window._sessionEndlessScrollEnabled=false;
|
||||
window._autoScrollFollow=true;
|
||||
window._composerControlVisibility=_composerControlVisibilityFromSettings(null);
|
||||
window._composerControlOrder=[];
|
||||
_applyComposerControlOrder(window._composerControlOrder);
|
||||
window._botName='Hermes';
|
||||
_bootSettings={check_for_updates:false};
|
||||
if(typeof setLocale==='function'){
|
||||
|
|
|
|||
|
|
@ -647,7 +647,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Sidebar tabs',
|
||||
settings_desc_tab_visibility: 'Choose which tabs appear in the sidebar and rail. Drag chips to reorder them. Chat and Settings are always visible.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -2310,7 +2310,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Schede della barra laterale',
|
||||
settings_desc_tab_visibility: 'Scegli quali schede mostrare nella barra laterale e nel rail. Chat e Impostazioni sono sempre visibili.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -3964,7 +3964,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'サイドバータブ',
|
||||
settings_desc_tab_visibility: 'サイドバーとレールに表示するタブを選択します。チャットと設定は常に表示されます。',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -6345,7 +6345,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Вкладки боковой панели',
|
||||
settings_desc_tab_visibility: 'Выберите, какие вкладки отображаются на боковой панели и в рейле. Чат и настройки всегда видны.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -7924,7 +7924,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Pestañas de la barra lateral',
|
||||
settings_desc_tab_visibility: 'Elige qué pestañas aparecen en la barra lateral y el rail. Chat y Configuración siempre están visibles.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -9184,7 +9184,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Seitenleiste-Tabs',
|
||||
settings_desc_tab_visibility: 'Wähle, welche Tabs in der Seitenleiste und im Rail angezeigt werden. Ziehe die Chips, um die Reihenfolge zu ändern. Chat und Einstellungen sind immer sichtbar.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -11117,7 +11117,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: '侧边栏标签',
|
||||
settings_desc_tab_visibility: '选择在侧边栏和导航栏中显示哪些标签。聊天和设置始终可见。',
|
||||
settings_label_composer_controls: '聊天底部控件',
|
||||
settings_desc_composer_controls: '切换显示在聊天底部的控件。不支持重新排序。',
|
||||
settings_desc_composer_controls: '切换显示在聊天底部的控件。拖动芯片可在各自的页脚分组内重新排序。',
|
||||
settings_label_composer_situational_controls: '情境化底部控件',
|
||||
settings_desc_composer_situational_controls: '这些控件并非始终显示,具体取决于视口、模式或运行时状态。如有需要,你仍可将其关闭。',
|
||||
composer_control_attach: '附加',
|
||||
|
|
@ -11983,7 +11983,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: '側邊欄標籤',
|
||||
settings_desc_tab_visibility: '選擇在側邊欄和導覽列中顯示哪些標籤。聊天和設定始終可見。',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -13534,7 +13534,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Abas da barra lateral',
|
||||
settings_desc_tab_visibility: 'Escolha quais abas aparecem na barra lateral e no rail. Chat e Configurações estão sempre visíveis.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -15087,7 +15087,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: '사이드바 탭',
|
||||
settings_desc_tab_visibility: '사이드바와 레일에 표시할 탭을 선택하세요. 채팅과 설정은 항상 표시됩니다.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -16753,7 +16753,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Onglets de la barre latérale',
|
||||
settings_desc_tab_visibility: 'Choisir quels onglets apparaissent dans la barre latérale et le rail. Chat et Paramètres sont toujours visibles. Faites glisser les puces pour les réorganiser.',
|
||||
settings_label_composer_controls: 'Contrôles du pied de page du chat',
|
||||
settings_desc_composer_controls: 'Activer/désactiver les contrôles affichés dans le pied de page du chat. La réorganisation n\'est pas prise en charge.',
|
||||
settings_desc_composer_controls: 'Activer/désactiver les contrôles affichés dans le pied de page du chat. Faites glisser les puces pour réorganiser chaque groupe du pied de page.',
|
||||
settings_label_composer_situational_controls: 'Contrôles contextuels du pied de page',
|
||||
settings_desc_composer_situational_controls: 'Ces contrôles n\'apparaissent pas toujours, selon la zone d\'affichage, le mode ou l\'état d\'exécution. Vous pouvez toujours les désactiver si nécessaire.',
|
||||
composer_control_attach: 'Joindre',
|
||||
|
|
@ -18375,7 +18375,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Kenar çubuğu sekmeleri',
|
||||
settings_desc_tab_visibility: 'Kenar çubuğunda ve rayda hangi sekmelerin görüneceğini seçin. Sohbet ve Ayarlar her zaman görünür durumdadır.',
|
||||
settings_label_composer_controls: 'Chat footer controls',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.',
|
||||
settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.',
|
||||
settings_label_composer_situational_controls: 'Situational footer controls',
|
||||
settings_desc_composer_situational_controls: 'These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.',
|
||||
composer_control_attach: 'Attach',
|
||||
|
|
@ -20043,7 +20043,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Karty paska bocznego',
|
||||
settings_desc_tab_visibility: 'Wybierz, które karty pojawiają się na pasku bocznym i szynie. Przeciągnij elementy, aby zmienić ich kolejność. Czat i Ustawienia są zawsze widoczne.',
|
||||
settings_label_composer_controls: 'Kontrolki stopki czatu',
|
||||
settings_desc_composer_controls: 'Przełączaj kontrolki wyświetlane w stopce czatu. Zmiana kolejności nie jest obsługiwana.',
|
||||
settings_desc_composer_controls: 'Przełączaj kontrolki wyświetlane w stopce czatu. Przeciągaj etykiety, aby zmieniać kolejność w każdej grupie stopki.',
|
||||
settings_label_composer_situational_controls: 'Sytuacyjne kontrolki stopki',
|
||||
settings_desc_composer_situational_controls: 'Te kontrolki nie zawsze się pojawiają, w zależności od obszaru roboczego, trybu lub stanu środowiska uruchomieniowego. Nadal możesz je wyłączyć, jeśli to konieczne.',
|
||||
composer_control_attach: 'Załącz',
|
||||
|
|
@ -21599,7 +21599,7 @@ const LOCALES = {
|
|||
settings_label_tab_visibility: 'Tab sidebar',
|
||||
settings_desc_tab_visibility: 'Chọn tab nào xuất hiện trong sidebar và thanh rail. Kéo các chip để đổi thứ tự. Chat và Settings luôn hiển thị.',
|
||||
settings_label_composer_controls: 'Điều khiển chân khung chat',
|
||||
settings_desc_composer_controls: 'Bật/tắt các điều khiển hiển thị ở chân khung chat. Không hỗ trợ đổi thứ tự.',
|
||||
settings_desc_composer_controls: 'Bật/tắt các điều khiển hiển thị ở chân khung chat. Kéo các chip để đổi thứ tự trong từng nhóm chân khung.',
|
||||
settings_label_composer_situational_controls: 'Điều khiển chân khung theo ngữ cảnh',
|
||||
settings_desc_composer_situational_controls: 'Các điều khiển này không phải lúc nào cũng xuất hiện, tùy theo kích thước màn hình, chế độ hoặc trạng thái chạy. Bạn vẫn có thể tắt chúng nếu cần.',
|
||||
composer_control_attach: 'Đính kèm',
|
||||
|
|
|
|||
|
|
@ -1101,7 +1101,7 @@
|
|||
<div class="settings-field">
|
||||
<label id="composerControlsLabel" data-i18n="settings_label_composer_controls">Chat footer controls</label>
|
||||
<div id="composerControlsChips" class="tab-visibility-chips" role="group" aria-labelledby="composerControlsLabel"></div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_composer_controls">Toggle which controls are shown in the chat footer. Reordering is not supported.</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_composer_controls">Toggle which controls are shown in the chat footer. Drag chips to reorder each footer group.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label id="composerSituationalControlsLabel" data-i18n="settings_label_composer_situational_controls">Situational footer controls</label>
|
||||
|
|
|
|||
157
static/panels.js
157
static/panels.js
|
|
@ -5865,6 +5865,10 @@ function _refreshProfileSwitchBackground(gen){
|
|||
if (typeof _applyTabOrder === 'function') _applyTabOrder(order);
|
||||
if (typeof _applyTabVisibility === 'function') _applyTabVisibility(hidden);
|
||||
_ensureComposerControlVisibilityState(s||{});
|
||||
if(Array.isArray(s&&s.composer_control_order)){
|
||||
const nextOrder=_setComposerControlOrder(s.composer_control_order);
|
||||
if(typeof window._applyComposerControlOrder==='function') window._applyComposerControlOrder(nextOrder);
|
||||
}
|
||||
_renderComposerControlChips();
|
||||
_renderComposerSituationalControlChips();
|
||||
if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings();
|
||||
|
|
@ -6708,7 +6712,10 @@ let _settingsPreferencesAutosaveRetryPayload = null;
|
|||
const _ALWAYS_VISIBLE_TABS = new Set(['chat','settings']);
|
||||
const _HIDDEN_TABS_LS_KEY = 'hermes-webui-hidden-tabs';
|
||||
const _TAB_ORDER_LS_KEY = 'hermes-webui-tab-order';
|
||||
const _COMPOSER_CONTROL_ORDER_LS_KEY = 'hermes-webui-composer-control-order';
|
||||
let _tabVisibilityDragSuppressUntil = 0;
|
||||
let _composerControlDragSuppressUntil = 0;
|
||||
let _composerControlDraggingKey = '';
|
||||
|
||||
function _sanitizeTabPanelList(panels){
|
||||
if(!Array.isArray(panels)) return [];
|
||||
|
|
@ -6940,11 +6947,123 @@ function _ensureComposerControlVisibilityState(settings){
|
|||
Object.assign(window._composerControlVisibility, fromSettings);
|
||||
}
|
||||
|
||||
function _composerControlVisibilityPayload(){
|
||||
const payload={};
|
||||
function _composerControlDefsForSettings(){
|
||||
const baseDefs=Array.isArray(window._COMPOSER_CONTROL_TOGGLE_DEFS)?window._COMPOSER_CONTROL_TOGGLE_DEFS:[];
|
||||
const situationalDefs=Array.isArray(window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS)?window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS:[];
|
||||
const defs=baseDefs.concat(situationalDefs);
|
||||
return baseDefs.concat(situationalDefs);
|
||||
}
|
||||
|
||||
function _getComposerControlOrder(){
|
||||
if(Array.isArray(window._composerControlOrder)){
|
||||
return typeof window._sanitizeComposerControlOrder==='function'
|
||||
? window._sanitizeComposerControlOrder(window._composerControlOrder)
|
||||
: window._composerControlOrder.slice();
|
||||
}
|
||||
try{
|
||||
const raw=localStorage.getItem(_COMPOSER_CONTROL_ORDER_LS_KEY);
|
||||
if(raw){
|
||||
const parsed=JSON.parse(raw);
|
||||
if(typeof window._sanitizeComposerControlOrder==='function') return window._sanitizeComposerControlOrder(parsed);
|
||||
if(Array.isArray(parsed)) return parsed.filter(key=>typeof key==='string');
|
||||
}
|
||||
}catch(e){}
|
||||
return [];
|
||||
}
|
||||
|
||||
function _setComposerControlOrder(order){
|
||||
const sanitized=typeof window._sanitizeComposerControlOrder==='function'
|
||||
? window._sanitizeComposerControlOrder(order)
|
||||
: (Array.isArray(order)?order.filter(key=>typeof key==='string') : []);
|
||||
window._composerControlOrder=sanitized;
|
||||
try{localStorage.setItem(_COMPOSER_CONTROL_ORDER_LS_KEY,JSON.stringify(sanitized));}catch(e){}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function _orderedComposerControlDefsForSettings(defs){
|
||||
defs=Array.isArray(defs)?defs:[];
|
||||
const byKey=new Map(defs.map(def=>[def.key,def]));
|
||||
const out=[];
|
||||
_getComposerControlOrder().forEach(function(key){
|
||||
if(byKey.has(key)) out.push(byKey.get(key));
|
||||
});
|
||||
defs.forEach(function(def){if(out.indexOf(def)===-1) out.push(def);});
|
||||
return out;
|
||||
}
|
||||
|
||||
function _composerControlOrderGroupKey(key){
|
||||
const def=_composerControlDefsForSettings().find(item=>item&&item.key===key);
|
||||
return def&&def.orderGroup?def.orderGroup:'';
|
||||
}
|
||||
|
||||
function _composerControlDropAllowed(sourceKey,targetKey){
|
||||
if(!sourceKey||!targetKey||sourceKey===targetKey) return false;
|
||||
const sourceGroup=_composerControlOrderGroupKey(sourceKey);
|
||||
const targetGroup=_composerControlOrderGroupKey(targetKey);
|
||||
return !!sourceGroup&&sourceGroup===targetGroup;
|
||||
}
|
||||
|
||||
function _clearComposerControlDragOver(){
|
||||
document.querySelectorAll('[data-composer-control-key].drag-over').forEach(function(el){el.classList.remove('drag-over');});
|
||||
}
|
||||
|
||||
function _moveComposerControlOrderKey(sourceKey,targetKey){
|
||||
if(!_composerControlDropAllowed(sourceKey,targetKey)) return false;
|
||||
const order=_orderedComposerControlDefsForSettings(_composerControlDefsForSettings()).map(def=>def.key);
|
||||
const from=order.indexOf(sourceKey);
|
||||
const to=order.indexOf(targetKey);
|
||||
if(from===-1||to===-1) return false;
|
||||
order.splice(from,1);
|
||||
order.splice(to,0,sourceKey);
|
||||
const next=_setComposerControlOrder(order);
|
||||
if(typeof window._applyComposerControlOrder==='function') window._applyComposerControlOrder(next);
|
||||
_renderComposerControlChips();
|
||||
_renderComposerSituationalControlChips();
|
||||
_scheduleAppearanceAutosave();
|
||||
return true;
|
||||
}
|
||||
|
||||
function _handleComposerControlChipDrop(e,targetKey){
|
||||
if(e){e.preventDefault();e.stopPropagation();}
|
||||
_clearComposerControlDragOver();
|
||||
const sourceKey=e&&e.dataTransfer?e.dataTransfer.getData('text/plain'):_composerControlDraggingKey;
|
||||
if(_moveComposerControlOrderKey(sourceKey,targetKey)) _composerControlDragSuppressUntil=Date.now()+250;
|
||||
_composerControlDraggingKey='';
|
||||
}
|
||||
|
||||
function _wireComposerControlChipDrag(chip,key){
|
||||
if(!chip)return;
|
||||
chip.setAttribute('data-composer-control-key',key);
|
||||
chip.setAttribute('draggable','true');
|
||||
chip.addEventListener('dragstart',function(e){
|
||||
_composerControlDraggingKey=key;
|
||||
chip.classList.add('dragging');
|
||||
if(e.dataTransfer){
|
||||
e.dataTransfer.effectAllowed='move';
|
||||
e.dataTransfer.setData('text/plain',key);
|
||||
}
|
||||
});
|
||||
chip.addEventListener('dragend',function(){
|
||||
chip.classList.remove('dragging');
|
||||
_clearComposerControlDragOver();
|
||||
_composerControlDraggingKey='';
|
||||
});
|
||||
chip.addEventListener('dragover',function(e){
|
||||
const sourceKey=_composerControlDraggingKey;
|
||||
if(!_composerControlDropAllowed(sourceKey,key)){
|
||||
if(e.dataTransfer)e.dataTransfer.dropEffect='none';
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
chip.classList.add('drag-over');
|
||||
if(e.dataTransfer)e.dataTransfer.dropEffect='move';
|
||||
});
|
||||
chip.addEventListener('dragleave',function(){chip.classList.remove('drag-over');});
|
||||
chip.addEventListener('drop',function(e){_handleComposerControlChipDrop(e,key);});
|
||||
}
|
||||
|
||||
function _composerControlVisibilityPayload(){
|
||||
const payload={};
|
||||
const defs=_composerControlDefsForSettings();
|
||||
const state=window._composerControlVisibility||{};
|
||||
defs.forEach(function(def){payload[def.key]=!!state[def.key];});
|
||||
return payload;
|
||||
|
|
@ -6974,7 +7093,7 @@ function _renderComposerControlChips(){
|
|||
const defs=Array.isArray(window._COMPOSER_CONTROL_TOGGLE_DEFS)?window._COMPOSER_CONTROL_TOGGLE_DEFS:[];
|
||||
const state=window._composerControlVisibility||{};
|
||||
container.innerHTML='';
|
||||
defs.forEach(function(def){
|
||||
_orderedComposerControlDefsForSettings(defs).forEach(function(def){
|
||||
const chip=document.createElement('button');
|
||||
chip.type='button';
|
||||
chip.className='tab-visibility-chip';
|
||||
|
|
@ -6983,7 +7102,8 @@ function _renderComposerControlChips(){
|
|||
chip.textContent=_composerControlChipLabel(def);
|
||||
chip.setAttribute('role','switch');
|
||||
chip.setAttribute('aria-checked',hidden?'false':'true');
|
||||
chip.onclick=function(){_toggleComposerControlChip(def.key);};
|
||||
chip.onclick=function(){if(Date.now()<_composerControlDragSuppressUntil)return;_toggleComposerControlChip(def.key);};
|
||||
_wireComposerControlChipDrag(chip,def.key);
|
||||
container.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
|
@ -6994,7 +7114,7 @@ function _renderComposerSituationalControlChips(){
|
|||
const defs=Array.isArray(window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS)?window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS:[];
|
||||
const state=window._composerControlVisibility||{};
|
||||
container.innerHTML='';
|
||||
defs.forEach(function(def){
|
||||
_orderedComposerControlDefsForSettings(defs).forEach(function(def){
|
||||
const chip=document.createElement('button');
|
||||
chip.type='button';
|
||||
chip.className='tab-visibility-chip';
|
||||
|
|
@ -7003,7 +7123,8 @@ function _renderComposerSituationalControlChips(){
|
|||
chip.textContent=_composerControlChipLabel(def);
|
||||
chip.setAttribute('role','switch');
|
||||
chip.setAttribute('aria-checked',hidden?'false':'true');
|
||||
chip.onclick=function(){_toggleComposerControlChip(def.key);};
|
||||
chip.onclick=function(){if(Date.now()<_composerControlDragSuppressUntil)return;_toggleComposerControlChip(def.key);};
|
||||
_wireComposerControlChipDrag(chip,def.key);
|
||||
container.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
|
@ -7527,6 +7648,7 @@ function _appearancePayloadFromUi(){
|
|||
worklog_details_expanded_default: worklogDetailsExpanded,
|
||||
activity_feed_expanded_default: worklogDetailsExpanded,
|
||||
..._composerControlVisibilityPayload(),
|
||||
composer_control_order: _getComposerControlOrder(),
|
||||
hidden_tabs: _getHiddenTabs(),
|
||||
tab_order: _getTabOrder(),
|
||||
};
|
||||
|
|
@ -7635,6 +7757,10 @@ async function _autosaveAppearanceSettings(payload){
|
|||
}
|
||||
if(saved){
|
||||
_ensureComposerControlVisibilityState(saved);
|
||||
if(Array.isArray(saved.composer_control_order)){
|
||||
const nextOrder=_setComposerControlOrder(saved.composer_control_order);
|
||||
if(typeof window._applyComposerControlOrder==='function') window._applyComposerControlOrder(nextOrder);
|
||||
}
|
||||
_renderComposerControlChips();
|
||||
_renderComposerSituationalControlChips();
|
||||
if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings();
|
||||
|
|
@ -8025,6 +8151,10 @@ async function loadSettingsPanel(){
|
|||
};
|
||||
}
|
||||
_ensureComposerControlVisibilityState(settings);
|
||||
if(Array.isArray(settings.composer_control_order)){
|
||||
const composerOrder=_setComposerControlOrder(settings.composer_control_order);
|
||||
if(typeof window._applyComposerControlOrder==='function') window._applyComposerControlOrder(composerOrder);
|
||||
}
|
||||
_renderComposerControlChips();
|
||||
_renderComposerSituationalControlChips();
|
||||
if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings();
|
||||
|
|
@ -10430,6 +10560,17 @@ function _applySavedSettingsUi(saved, body, opts){
|
|||
else if(typeof _applyBusyComposerPlaceholder==='function') _applyBusyComposerPlaceholder();
|
||||
if(typeof setLocale==='function') setLocale(language);
|
||||
if(typeof applyLocaleToDOM==='function') applyLocaleToDOM();
|
||||
_ensureComposerControlVisibilityState(saved||body||{});
|
||||
const composerOrderSource=(saved&&Array.isArray(saved.composer_control_order))
|
||||
? saved.composer_control_order
|
||||
: (Array.isArray(body.composer_control_order)?body.composer_control_order:null);
|
||||
if(composerOrderSource){
|
||||
const composerOrder=_setComposerControlOrder(composerOrderSource);
|
||||
if(typeof window._applyComposerControlOrder==='function') window._applyComposerControlOrder(composerOrder);
|
||||
}
|
||||
_renderComposerControlChips();
|
||||
_renderComposerSituationalControlChips();
|
||||
if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings();
|
||||
const maxTokensField=$('settingsMaxTokens');
|
||||
if(maxTokensField){
|
||||
const savedRawMaxTokens=saved&&saved.max_tokens;
|
||||
|
|
@ -10994,6 +11135,8 @@ async function saveSettings(andClose){
|
|||
body.large_text_paste_as_attachment=!!($('settingsLargeTextPasteAsAttachment')||{}).checked;
|
||||
body.project_quick_create_buttons=!!($('settingsProjectQuickCreate')||{}).checked;
|
||||
Object.assign(body,_structuredCodeViewFromUi());
|
||||
Object.assign(body,_composerControlVisibilityPayload());
|
||||
body.composer_control_order=_getComposerControlOrder();
|
||||
body.language=language;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
const maxTokensField=$('settingsMaxTokens');
|
||||
|
|
|
|||
|
|
@ -4885,6 +4885,8 @@ main.main > #mainPlugin{display:none;}
|
|||
.tab-visibility-chip:focus-visible{outline:2px solid var(--link,var(--accent));outline-offset:2px;}
|
||||
.tab-visibility-chip.chip-off{background:transparent;color:var(--muted);border-color:var(--border);font-weight:400;}
|
||||
.tab-visibility-chip.chip-off:hover{border-color:var(--muted);color:var(--text);}
|
||||
#composerControlsChips .tab-visibility-chip,
|
||||
#composerSituationalControlsChips .tab-visibility-chip{touch-action:none;-webkit-user-select:none;-webkit-touch-callout:none;}
|
||||
.composer-control-hidden{display:none!important;}
|
||||
|
||||
/* Selects & text/password inputs — uniform. Uses !important to win
|
||||
|
|
|
|||
|
|
@ -7,14 +7,21 @@ toggle is wired end-to-end (config boolean key -> boot.js definition + read-back
|
|||
key exists across all locale blocks, so a future refactor can't silently orphan
|
||||
a control or break locale parity.
|
||||
"""
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
# The 15 composer-control visibility flags this feature ships.
|
||||
HIDE_KEYS = [
|
||||
|
|
@ -59,6 +66,22 @@ I18N_KEYS = [
|
|||
]
|
||||
|
||||
|
||||
def _extract_function(source: str, name: str) -> str:
|
||||
marker = f"function {name}("
|
||||
start = source.index(marker)
|
||||
brace = source.index("{", start)
|
||||
depth = 0
|
||||
for idx in range(brace, len(source)):
|
||||
char = source[idx]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : idx + 1]
|
||||
raise AssertionError(f"Could not extract {name}")
|
||||
|
||||
|
||||
def test_all_hide_flags_registered_as_boolean_settings_keys():
|
||||
"""Every toggle must be in config.py's boolean-keys set so it persists and
|
||||
round-trips through save/load."""
|
||||
|
|
@ -83,6 +106,38 @@ def test_footer_control_chips_rendered_in_panels():
|
|||
assert "_applyComposerFooterVisibilitySettings" in PANELS_JS
|
||||
|
||||
|
||||
def test_composer_control_order_frontend_contracts():
|
||||
"""Footer reorder must coexist with the now-required situational chips."""
|
||||
assert "composer_control_order" in CONFIG_PY
|
||||
assert "composer_control_order" in BOOT_JS
|
||||
assert "composer_control_order" in PANELS_JS
|
||||
assert "_sanitizeComposerControlOrder" in BOOT_JS
|
||||
assert "_orderedComposerControlDefs" in BOOT_JS
|
||||
assert "_applyComposerControlOrder" in BOOT_JS
|
||||
assert "orderSelector" in BOOT_JS
|
||||
assert "orderGroup" in BOOT_JS
|
||||
assert "_COMPOSER_CONTROL_ORDER_LS_KEY" in PANELS_JS
|
||||
assert "_orderedComposerControlDefsForSettings" in PANELS_JS
|
||||
assert "_wireComposerControlChipDrag" in PANELS_JS
|
||||
assert "_moveComposerControlOrderKey" in PANELS_JS
|
||||
assert "_composerControlDropAllowed" in PANELS_JS
|
||||
assert "_renderComposerSituationalControlChips" in PANELS_JS
|
||||
assert "composerSituationalControlsChips" in PANELS_JS
|
||||
assert "#composerControlsChips .tab-visibility-chip" in STYLE_CSS
|
||||
assert "#composerSituationalControlsChips .tab-visibility-chip" in STYLE_CSS
|
||||
|
||||
render_primary = _extract_function(PANELS_JS, "_renderComposerControlChips")
|
||||
render_situational = _extract_function(PANELS_JS, "_renderComposerSituationalControlChips")
|
||||
for body in (render_primary, render_situational):
|
||||
assert "_orderedComposerControlDefsForSettings" in body
|
||||
assert "_wireComposerControlChipDrag" in body
|
||||
|
||||
move_body = _extract_function(PANELS_JS, "_moveComposerControlOrderKey")
|
||||
assert "_renderComposerControlChips()" in move_body
|
||||
assert "_renderComposerSituationalControlChips()" in move_body
|
||||
assert "_scheduleAppearanceAutosave()" in move_body
|
||||
|
||||
|
||||
def test_new_i18n_keys_exist_across_all_locale_blocks():
|
||||
"""Every new i18n key must appear in all 13 locale blocks (strict locale
|
||||
parity), not just `en` — otherwise the locale-coverage suite goes red."""
|
||||
|
|
@ -93,3 +148,102 @@ def test_new_i18n_keys_exist_across_all_locale_blocks():
|
|||
f"{key} appears {count}x in i18n.js — expected >=13 (one per locale "
|
||||
f"block) for strict locale parity"
|
||||
)
|
||||
|
||||
assert "Drag chips to reorder each footer group." in INDEX_HTML
|
||||
assert "Reordering is not supported." not in INDEX_HTML
|
||||
assert "Reordering is not supported." not in I18N_JS
|
||||
assert "不支持重新排序" not in I18N_JS
|
||||
|
||||
|
||||
def test_composer_control_order_is_validated_and_deduped():
|
||||
"""The backend persists only known composer control order keys."""
|
||||
import api.config as config
|
||||
|
||||
original_settings_file = config.SETTINGS_FILE
|
||||
test_state_dir = ROOT / ".tmp-test-issue4598"
|
||||
try:
|
||||
config.SETTINGS_FILE = test_state_dir / "settings.json"
|
||||
test_state_dir.mkdir(exist_ok=True)
|
||||
saved = config.save_settings(
|
||||
{
|
||||
"composer_control_order": [
|
||||
"hide_composer_model",
|
||||
"bogus",
|
||||
"hide_composer_attach",
|
||||
"hide_composer_model",
|
||||
42,
|
||||
"hide_composer_context",
|
||||
]
|
||||
}
|
||||
)
|
||||
assert saved["composer_control_order"] == [
|
||||
"hide_composer_model",
|
||||
"hide_composer_attach",
|
||||
"hide_composer_context",
|
||||
]
|
||||
persisted = json.loads(config.SETTINGS_FILE.read_text(encoding="utf-8"))
|
||||
assert persisted["composer_control_order"] == saved["composer_control_order"]
|
||||
finally:
|
||||
config.SETTINGS_FILE = original_settings_file
|
||||
shutil.rmtree(test_state_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_composer_control_order_move_updates_both_chip_groups():
|
||||
"""A same-footer-group drag saves order and re-renders both chip groups."""
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node is required for the frontend behavior harness")
|
||||
|
||||
helpers = "\n".join(
|
||||
_extract_function(PANELS_JS, name)
|
||||
for name in [
|
||||
"_composerControlDefsForSettings",
|
||||
"_getComposerControlOrder",
|
||||
"_setComposerControlOrder",
|
||||
"_orderedComposerControlDefsForSettings",
|
||||
"_composerControlOrderGroupKey",
|
||||
"_composerControlDropAllowed",
|
||||
"_moveComposerControlOrderKey",
|
||||
]
|
||||
)
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
global.window = {{}};
|
||||
global.localStorage = {{
|
||||
_items: {{}},
|
||||
getItem(key) {{ return this._items[key] || null; }},
|
||||
setItem(key, value) {{ this._items[key] = String(value); }},
|
||||
}};
|
||||
const _COMPOSER_CONTROL_ORDER_LS_KEY = 'hermes-webui-composer-control-order';
|
||||
window._COMPOSER_CONTROL_TOGGLE_DEFS = [
|
||||
{{ key: 'hide_composer_attach', orderGroup: 'left' }},
|
||||
{{ key: 'hide_composer_context', orderGroup: 'right' }},
|
||||
];
|
||||
window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS = [
|
||||
{{ key: 'hide_composer_voice_mode', orderGroup: 'left' }},
|
||||
{{ key: 'hide_composer_status', orderGroup: 'right' }},
|
||||
];
|
||||
window._sanitizeComposerControlOrder = function(order) {{
|
||||
const allowed = new Set(window._COMPOSER_CONTROL_TOGGLE_DEFS.concat(window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS).map(def => def.key));
|
||||
const out = [];
|
||||
(Array.isArray(order) ? order : []).forEach(key => {{
|
||||
if (allowed.has(key) && !out.includes(key)) out.push(key);
|
||||
}});
|
||||
return out;
|
||||
}};
|
||||
window._applyComposerControlOrder = function(order) {{ window._appliedOrder = order.slice(); }};
|
||||
let primaryRenders = 0;
|
||||
let situationalRenders = 0;
|
||||
let autosaves = 0;
|
||||
function _renderComposerControlChips() {{ primaryRenders += 1; }}
|
||||
function _renderComposerSituationalControlChips() {{ situationalRenders += 1; }}
|
||||
function _scheduleAppearanceAutosave() {{ autosaves += 1; }}
|
||||
{helpers}
|
||||
if (!_moveComposerControlOrderKey('hide_composer_voice_mode', 'hide_composer_attach')) throw new Error('same-group move failed');
|
||||
if (JSON.stringify(window._composerControlOrder) !== JSON.stringify(['hide_composer_voice_mode','hide_composer_attach','hide_composer_context','hide_composer_status'])) throw new Error('unexpected order ' + JSON.stringify(window._composerControlOrder));
|
||||
if (primaryRenders !== 1 || situationalRenders !== 1 || autosaves !== 1) throw new Error('render/autosave counts wrong');
|
||||
if (_moveComposerControlOrderKey('hide_composer_status', 'hide_composer_attach')) throw new Error('cross-group move should be rejected');
|
||||
"""
|
||||
)
|
||||
result = subprocess.run([node, "-e", script], cwd=ROOT, text=True, capture_output=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
|
|
|||
Loading…
Reference in a new issue