diff --git a/CHANGELOG.md b/CHANGELOG.md index 94045f90..9e53ee0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.563] — 2026-06-21 — Release TV (per-control composer footer visibility) + +### Added + +- **Composer footer controls can now be shown or hidden individually from Settings → Appearance.** The footer controls are grouped into "primary" and "situational" sets, and each (attach, saved prompts, mic, voice mode, YOLO, profile, workspace, model, reasoning, context, toolsets, status, quota chip, background badge, mobile config) can be toggled on or off. Preferences persist per profile and apply live without a reload. Purely additive visibility toggles — the footer layout itself is unchanged. Thanks @Paladin173. (#4598) + ## [v0.51.562] — 2026-06-21 — Release TU (selected-context quote cards) ### Added diff --git a/api/config.py b/api/config.py index 1528a6bc..62e1756d 100644 --- a/api/config.py +++ b/api/config.py @@ -7143,6 +7143,21 @@ _SETTINGS_DEFAULTS = { "chat_activity_display_mode": "compact_worklog", # compact_worklog | transparent_stream "auto_scroll_follow": True, # follow new output to the bottom while streaming (Codex/Claude-Code-style sticky bottom); the user scrolling up unpins and is respected "worklog_details_expanded_default": False, # opt-in: expand Worklog details by default; default remains folded + "hide_composer_attach": False, # hide attach button in composer footer + "hide_composer_saved_prompts": False, # hide saved prompts button in composer footer + "hide_composer_mic": False, # hide dictation mic button in composer footer + "hide_composer_voice_mode": False, # hide hands-free voice-mode button in composer footer + "hide_composer_yolo": False, # hide YOLO chip in composer footer + "hide_composer_profile": False, # hide profile chip in composer footer + "hide_composer_workspace": False, # hide workspace controls in composer footer/mobile config panel + "hide_composer_mobile_config": False, # hide mobile composer config button + "hide_composer_model": False, # hide model chip in composer footer/mobile config panel + "hide_composer_quota_chip": False, # hide provider quota chip in composer footer + "hide_composer_reasoning": False, # hide reasoning chip in composer footer/mobile config panel + "hide_composer_toolsets": False, # hide toolsets chip in composer footer + "hide_composer_status": False, # hide status text in composer footer + "hide_composer_context": False, # hide context indicator in composer footer/mobile config panel + "hide_composer_bg_badge": False, # hide background-jobs badge in composer footer "pinned_sessions_limit": 3, # maximum active pinned sessions shown in the sidebar "inflight_state_max_sessions": 8, # max active-stream recovery snapshots kept in browser localStorage "inflight_state_max_messages": 24, # max recent messages kept per recovery snapshot @@ -7370,6 +7385,21 @@ _SETTINGS_BOOL_KEYS = { "auto_scroll_follow", "worklog_details_expanded_default", "auth_disabled_acknowledged", + "hide_composer_attach", + "hide_composer_saved_prompts", + "hide_composer_mic", + "hide_composer_voice_mode", + "hide_composer_yolo", + "hide_composer_profile", + "hide_composer_workspace", + "hide_composer_mobile_config", + "hide_composer_model", + "hide_composer_quota_chip", + "hide_composer_reasoning", + "hide_composer_toolsets", + "hide_composer_status", + "hide_composer_context", + "hide_composer_bg_badge", } # Language codes are validated as short alphanumeric BCP-47-like tags (e.g. 'en', 'zh', 'fr') _SETTINGS_LANG_RE = __import__("re").compile(r"^[a-zA-Z]{2,10}(-[a-zA-Z0-9]{2,8})?$") diff --git a/static/boot.js b/static/boot.js index e6ed8a39..9b399cd4 100644 --- a/static/boot.js +++ b/static/boot.js @@ -1921,6 +1921,80 @@ function applyBotName(){ if(msg) msg.placeholder='Message '+name+'\u2026'; } +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']}, +]; +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']}, + {key:'hide_composer_toolsets',label:'Toolsets',labelKey:'composer_control_toolsets',selectors:['#composerToolsetsWrap']}, + {key:'hide_composer_status',label:'Status',labelKey:'composer_control_status',selectors:['#composerStatus']}, +]; +window._COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS=_COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS; + +function _allComposerControlToggleDefs(){ + return _COMPOSER_CONTROL_TOGGLE_DEFS.concat(_COMPOSER_SITUATIONAL_CONTROL_TOGGLE_DEFS); +} + +function _composerControlVisibilityFromSettings(settings){ + const next={}; + for(const def of _allComposerControlToggleDefs()){ + next[def.key]=!!(settings&&settings[def.key]); + } + return next; +} +window._composerControlVisibilityFromSettings=_composerControlVisibilityFromSettings; + +function _setComposerControlHidden(el, hidden){ + if(!el) return; + el.classList.toggle('composer-control-hidden', !!hidden); + if(hidden) el.setAttribute('aria-hidden','true'); + else el.removeAttribute('aria-hidden'); +} + +function _applyComposerFooterVisibilitySettings(){ + const hidden=window._composerControlVisibility||{}; + for(const def of _allComposerControlToggleDefs()){ + const isHidden=!!hidden[def.key]; + for(const selector of def.selectors){ + document.querySelectorAll(selector).forEach(el=>_setComposerControlHidden(el,isHidden)); + } + } + + const hideMic=!!hidden.hide_composer_mic; + if(hideMic&&window._micActive&&typeof window._stopMic==='function'){ + try{window._stopMic();}catch(_){ } + } + + const hideSavedPrompts=!!hidden.hide_composer_saved_prompts; + const savedBtn=$('btnSavedPrompts'); + const savedPopup=$('savedPromptsPopup'); + if(hideSavedPrompts&&savedPopup){ + savedPopup.style.display='none'; + if(savedBtn) savedBtn.setAttribute('aria-expanded','false'); + } + + if(hidden.hide_composer_workspace&&typeof closeWsDropdown==='function') closeWsDropdown(); + if(hidden.hide_composer_profile&&typeof closeProfileDropdown==='function') closeProfileDropdown(); + if(hidden.hide_composer_model&&typeof closeModelDropdown==='function') closeModelDropdown(); + if(hidden.hide_composer_reasoning&&typeof closeReasoningDropdown==='function') closeReasoningDropdown(); + if(hidden.hide_composer_toolsets&&typeof closeToolsetsDropdown==='function') closeToolsetsDropdown(); + if(hidden.hide_composer_mobile_config&&typeof closeMobileComposerConfig==='function') closeMobileComposerConfig(); +} +window._applyComposerFooterVisibilitySettings=_applyComposerFooterVisibilitySettings; + (async()=>{ // Load send key preference let _bootSettings={}; @@ -1973,6 +2047,7 @@ function applyBotName(){ window._busyInputMode=(s.busy_input_mode||'queue'); window._sessionEndlessScrollEnabled=!!s.session_endless_scroll; window._autoScrollFollow=s.auto_scroll_follow!==false; + window._composerControlVisibility=_composerControlVisibilityFromSettings(s); window._botName=s.bot_name||'Hermes'; if(s.default_model_provider) window._activeProvider=s.default_model_provider; if(s.default_model){ @@ -2043,6 +2118,7 @@ function applyBotName(){ setLocale(_lang); if(typeof applyLocaleToDOM==='function')applyLocaleToDOM(); } + _applyComposerFooterVisibilitySettings(); // TTS: apply enabled state on boot so buttons show/hide correctly (#499) if(typeof _applyTtsEnabled==='function') _applyTtsEnabled(localStorage.getItem('hermes-tts-enabled')==='true'); }catch(e){ @@ -2074,6 +2150,7 @@ function applyBotName(){ window._busyInputMode='queue'; window._sessionEndlessScrollEnabled=false; window._autoScrollFollow=true; + window._composerControlVisibility=_composerControlVisibilityFromSettings(null); window._botName='Hermes'; _bootSettings={check_for_updates:false}; if(typeof setLocale==='function'){ @@ -2083,6 +2160,7 @@ function applyBotName(){ setLocale(_lang); if(typeof applyLocaleToDOM==='function')applyLocaleToDOM(); } + _applyComposerFooterVisibilitySettings(); if(typeof _applyTtsEnabled==='function') _applyTtsEnabled(localStorage.getItem('hermes-tts-enabled')==='true'); } // Non-blocking update check (fire-and-forget, once per tab session) diff --git a/static/i18n.js b/static/i18n.js index 3ca43ce2..53b4c460 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -612,6 +612,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Open in browser', settings_dropdown_conversation: 'Conversation', settings_dropdown_appearance: 'Appearance', @@ -2162,6 +2181,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Apri nel browser', settings_dropdown_conversation: 'Conversazione', settings_dropdown_appearance: 'Aspetto', @@ -3703,6 +3741,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'ブラウザで開く', settings_dropdown_conversation: '会話', settings_dropdown_appearance: '外観', @@ -5944,6 +6001,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Открыть в браузере', settings_section_system_title: 'System', settings_tab_appearance: 'Appearance', @@ -7410,6 +7486,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Abrir en el navegador', settings_section_system_title: 'System', settings_tab_appearance: 'Appearance', @@ -8557,6 +8652,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', workspace_drag_hint: 'Ziehen zum Neuordnen', workspace_reorder_failed: 'Neuordnen fehlgeschlagen', @@ -10377,6 +10491,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: '在浏览器中打开', settings_section_system_title: '系统', settings_tab_appearance: '外观', @@ -11157,6 +11290,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: '在瀏覽器中開啟', settings_dropdown_conversation: '對話', settings_dropdown_appearance: '外觀', @@ -12594,6 +12746,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Abrir no navegador', settings_dropdown_conversation: 'Conversa', settings_dropdown_appearance: 'Aparência', @@ -14037,6 +14208,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: '브라우저에서 열기', settings_dropdown_conversation: '대화', settings_dropdown_appearance: '외형', @@ -15502,6 +15692,25 @@ const LOCALES = { settings_label_tab_visibility: 'Onglets de la barre latérale', settings_desc_tab_visibility: 'Choisissez quels onglets apparaissent dans la barre latérale et le rail. Chat et Paramètres sont toujours 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Ouvrir dans le navigateur', settings_dropdown_conversation: 'Conversation', settings_dropdown_appearance: 'Apparence', @@ -17061,6 +17270,25 @@ 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_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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Tarayıcıda aç', settings_dropdown_conversation: 'Konuşma', settings_dropdown_appearance: 'Dış görünüş', @@ -18616,6 +18844,25 @@ 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: 'Chat footer controls', + settings_desc_composer_controls: 'Toggle which controls are shown in the chat footer. Reordering is not supported.', + 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', + composer_control_saved_prompts: 'Saved prompts', + composer_control_mic: 'Mic', + composer_control_profile: 'Profile', + composer_control_workspace: 'Workspace', + composer_control_model: 'Model', + composer_control_reasoning: 'Reasoning', + composer_control_context: 'Context', + composer_control_voice_mode: 'Voice mode', + composer_control_yolo: 'YOLO', + composer_control_bg_badge: 'Background badge', + composer_control_mobile_config: 'Mobile config', + composer_control_quota_chip: 'Quota chip', + composer_control_toolsets: 'Toolsets', + composer_control_status: 'Status', open_in_browser: 'Otwórz w przeglądarce', settings_dropdown_conversation: 'Konwersacja', settings_dropdown_appearance: 'Wygląd', diff --git a/static/index.html b/static/index.html index 22707d43..3a601bf6 100644 --- a/static/index.html +++ b/static/index.html @@ -1075,6 +1075,16 @@
Choose which tabs appear in the sidebar and rail. Drag chips to reorder them. Chat and Settings are always visible.
+
+ +
+
Toggle which controls are shown in the chat footer. Reordering is not supported.
+
+
+ +
+
These controls do not always appear, depending on viewport, mode, or runtime state. You can still toggle them off if needed.
+
diff --git a/static/panels.js b/static/panels.js index 871c18a1..1b8e228d 100644 --- a/static/panels.js +++ b/static/panels.js @@ -5556,6 +5556,10 @@ function _refreshProfileSwitchBackground(gen){ if (typeof _setTabOrder === 'function') _setTabOrder(order); if (typeof _applyTabOrder === 'function') _applyTabOrder(order); if (typeof _applyTabVisibility === 'function') _applyTabVisibility(hidden); + _ensureComposerControlVisibilityState(s||{}); + _renderComposerControlChips(); + _renderComposerSituationalControlChips(); + if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings(); }).catch(function(){}); } @@ -6412,6 +6416,82 @@ function _toggleTabVisibilityChip(panel){ _scheduleAppearanceAutosave(); } +function _ensureComposerControlVisibilityState(settings){ + const fromSettings=(typeof _composerControlVisibilityFromSettings==='function') + ? _composerControlVisibilityFromSettings(settings||{}) + : {}; + if(!window._composerControlVisibility) window._composerControlVisibility={}; + Object.assign(window._composerControlVisibility, fromSettings); +} + +function _composerControlVisibilityPayload(){ + const payload={}; + 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); + const state=window._composerControlVisibility||{}; + defs.forEach(function(def){payload[def.key]=!!state[def.key];}); + return payload; +} + +function _toggleComposerControlChip(key){ + if(!window._composerControlVisibility) window._composerControlVisibility={}; + window._composerControlVisibility[key]=!window._composerControlVisibility[key]; + if(typeof _renderComposerControlChips==='function') _renderComposerControlChips(); + if(typeof _renderComposerSituationalControlChips==='function') _renderComposerSituationalControlChips(); + if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings(); + _scheduleAppearanceAutosave(); +} + +function _composerControlChipLabel(def){ + if(!def) return ''; + if(def.labelKey&&typeof t==='function'){ + const localized=t(def.labelKey); + if(typeof localized==='string'&&localized&&localized!==def.labelKey) return localized; + } + return def.label||''; +} + +function _renderComposerControlChips(){ + const container=$('composerControlsChips'); + if(!container) return; + const defs=Array.isArray(window._COMPOSER_CONTROL_TOGGLE_DEFS)?window._COMPOSER_CONTROL_TOGGLE_DEFS:[]; + const state=window._composerControlVisibility||{}; + container.innerHTML=''; + defs.forEach(function(def){ + const chip=document.createElement('button'); + chip.type='button'; + chip.className='tab-visibility-chip'; + const hidden=!!state[def.key]; + if(hidden) chip.classList.add('chip-off'); + chip.textContent=_composerControlChipLabel(def); + chip.setAttribute('role','switch'); + chip.setAttribute('aria-checked',hidden?'false':'true'); + chip.onclick=function(){_toggleComposerControlChip(def.key);}; + container.appendChild(chip); + }); +} + +function _renderComposerSituationalControlChips(){ + const container=$('composerSituationalControlsChips'); + if(!container) return; + 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){ + const chip=document.createElement('button'); + chip.type='button'; + chip.className='tab-visibility-chip'; + const hidden=!!state[def.key]; + if(hidden) chip.classList.add('chip-off'); + chip.textContent=_composerControlChipLabel(def); + chip.setAttribute('role','switch'); + chip.setAttribute('aria-checked',hidden?'false':'true'); + chip.onclick=function(){_toggleComposerControlChip(def.key);}; + container.appendChild(chip); + }); +} + function switchSettingsSection(name,opts){ // If the main content is not showing settings, just remember the section // without force-switching the panel. The section will be applied when the @@ -6738,6 +6818,7 @@ function _appearancePayloadFromUi(){ render_user_markdown: !!($('settingsRenderUserMarkdown')||{}).checked, worklog_details_expanded_default: worklogDetailsExpanded, activity_feed_expanded_default: worklogDetailsExpanded, + ..._composerControlVisibilityPayload(), hidden_tabs: _getHiddenTabs(), tab_order: _getTabOrder(), }; @@ -6832,6 +6913,12 @@ async function _autosaveAppearanceSettings(payload){ : saved.activity_feed_expanded_default ); } + if(saved){ + _ensureComposerControlVisibilityState(saved); + _renderComposerControlChips(); + _renderComposerSituationalControlChips(); + if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings(); + } _setAppearanceAutosaveStatus('saved'); }catch(e){ console.warn('[settings] appearance autosave failed', e); @@ -7120,6 +7207,10 @@ async function loadSettingsPanel(){ _scheduleAppearanceAutosave(); }; } + _ensureComposerControlVisibilityState(settings); + _renderComposerControlChips(); + _renderComposerSituationalControlChips(); + if(typeof _applyComposerFooterVisibilitySettings==='function') _applyComposerFooterVisibilitySettings(); // Tab visibility/order chips (dynamically populated from DOM) var hiddenTabs=[]; if(Array.isArray(settings.hidden_tabs)){ diff --git a/static/style.css b/static/style.css index e6932025..02130c58 100644 --- a/static/style.css +++ b/static/style.css @@ -4239,6 +4239,7 @@ 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);} +.composer-control-hidden{display:none!important;} /* Selects & text/password inputs — uniform. Uses !important to win over legacy inline styles that were written for the modal era. */ diff --git a/static/ui.js b/static/ui.js index eac583cc..401fb30f 100644 --- a/static/ui.js +++ b/static/ui.js @@ -4110,6 +4110,12 @@ function _syncCtxIndicator(usage){ const wrap=$('ctxIndicatorWrap'); const el=$('ctxIndicator'); if(!el)return; + const ctxHidden=!!(window._composerControlVisibility&&window._composerControlVisibility.hide_composer_context); + if(ctxHidden){ + if(wrap) wrap.style.display='none'; + _syncMobileCtxDisplay({visible:false}); + return; + } // #1436: Use last_prompt_tokens only — NEVER fall back to cumulative // input_tokens for the "context window % used" calculation. input_tokens // is summed across all turns, so dividing it by the context window gives a @@ -4130,7 +4136,12 @@ function _syncCtxIndicator(usage){ _syncMobileCtxDisplay({visible:false}); return; } - if(wrap) wrap.style.display=''; + if(wrap){ + // Defensive reset: keep dynamic context display from being stuck hidden. + wrap.classList.remove('composer-control-hidden'); + wrap.removeAttribute('aria-hidden'); + wrap.style.display=''; + } const hasPromptTok=!!promptTok; const rawPct=hasPromptTok?Math.round((promptTok/ctxWindow)*100):0; const pct=Math.min(100,rawPct); @@ -5284,11 +5295,20 @@ function setStatus(t){ function setComposerStatus(t){ const el=$('composerStatus'); if(!el)return; + const statusHidden=!!(window._composerControlVisibility&&window._composerControlVisibility.hide_composer_status); + if(statusHidden){ + el.style.display='none'; + el.textContent=''; + return; + } if(!t){ el.style.display='none'; el.textContent=''; return; } + // Defensive reset: a stale hidden class should never block live status text. + el.classList.remove('composer-control-hidden'); + el.removeAttribute('aria-hidden'); el.textContent=t; el.style.display=''; } diff --git a/tests/test_issue4598_composer_control_visibility.py b/tests/test_issue4598_composer_control_visibility.py new file mode 100644 index 00000000..b242d4ef --- /dev/null +++ b/tests/test_issue4598_composer_control_visibility.py @@ -0,0 +1,95 @@ +"""Static-contract regression tests for the composer footer-control visibility +toggles (#4598). + +Mirrors the sibling pattern in test_sidebar_tab_visibility.py: pins that each +toggle is wired end-to-end (config boolean key -> boot.js definition + read-back +-> index.html control -> panels.js chip render -> apply) and that every new i18n +key exists across all locale blocks, so a future refactor can't silently orphan +a control or break locale parity. +""" +from pathlib import Path + +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") + +# The 15 composer-control visibility flags this feature ships. +HIDE_KEYS = [ + "hide_composer_attach", + "hide_composer_saved_prompts", + "hide_composer_mic", + "hide_composer_voice_mode", + "hide_composer_yolo", + "hide_composer_profile", + "hide_composer_workspace", + "hide_composer_mobile_config", + "hide_composer_model", + "hide_composer_quota_chip", + "hide_composer_reasoning", + "hide_composer_toolsets", + "hide_composer_status", + "hide_composer_context", + "hide_composer_bg_badge", +] + +# The new i18n keys the feature adds (section labels/descriptions + per-chip labels). +I18N_KEYS = [ + "settings_label_composer_controls", + "settings_desc_composer_controls", + "settings_label_composer_situational_controls", + "settings_desc_composer_situational_controls", + "composer_control_attach", + "composer_control_saved_prompts", + "composer_control_mic", + "composer_control_profile", + "composer_control_workspace", + "composer_control_model", + "composer_control_reasoning", + "composer_control_context", + "composer_control_voice_mode", + "composer_control_yolo", + "composer_control_bg_badge", + "composer_control_mobile_config", + "composer_control_quota_chip", + "composer_control_toolsets", + "composer_control_status", +] + + +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.""" + for key in HIDE_KEYS: + assert f'"{key}"' in CONFIG_PY, f"{key} missing from config.py boolean settings keys" + + +def test_hide_composer_send_orphan_key_fully_removed(): + """The re-push removed the orphaned hide_composer_send key (Send is always + visible) — it must not linger anywhere.""" + assert "hide_composer_send" not in CONFIG_PY + assert "hide_composer_send" not in BOOT_JS + assert "hide_composer_send" not in PANELS_JS + + +def test_footer_control_chips_rendered_in_panels(): + """panels.js must render the primary + situational control chips and apply + the visibility settings live.""" + assert "_renderComposerControlChips" in PANELS_JS + assert "_renderComposerSituationalControlChips" in PANELS_JS + assert "_ensureComposerControlVisibilityState" in PANELS_JS + assert "_applyComposerFooterVisibilitySettings" in PANELS_JS + + +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.""" + # 13 locale blocks (en + 12). Each key should appear at least 13 times. + for key in I18N_KEYS: + count = I18N_JS.count(f"{key}:") + assert count >= 13, ( + f"{key} appears {count}x in i18n.js — expected >=13 (one per locale " + f"block) for strict locale parity" + )