From d8f0f8f97f9cfb327f2cc1c50c6888143eb4f253 Mon Sep 17 00:00:00 2001 From: Paladin173 <35980893+Paladin173@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:11:54 -0500 Subject: [PATCH] Fix composer control reorder rebase collision Reapply footer control ordering on current upstream/master while preserving the required situational chip renderer. Persist composer_control_order with backend validation, keep the settings descriptions reorder-aware, and make primary/situational chip renderers participate in same-group drag ordering. Verified with: node --check static/boot.js; node --check static/panels.js; git diff --check; ./scripts/test.sh tests/test_issue4598_composer_control_visibility.py --- api/config.py | 18 +- static/boot.js | 85 ++++++++-- static/i18n.js | 28 ++-- static/index.html | 2 +- static/panels.js | 157 +++++++++++++++++- static/style.css | 2 + ...t_issue4598_composer_control_visibility.py | 154 +++++++++++++++++ 7 files changed, 404 insertions(+), 42 deletions(-) diff --git a/api/config.py b/api/config.py index 19721d49..f8577a8c 100644 --- a/api/config.py +++ b/api/config.py @@ -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) diff --git a/static/boot.js b/static/boot.js index 56efabc4..e40d2858 100644 --- a/static/boot.js +++ b/static/boot.js @@ -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'){ diff --git a/static/i18n.js b/static/i18n.js index 5f0f5693..a8c54f83 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -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', diff --git a/static/index.html b/static/index.html index fab84cd3..8a62f4da 100644 --- a/static/index.html +++ b/static/index.html @@ -1101,7 +1101,7 @@