diff --git a/static/boot.js b/static/boot.js index 9b2d4cb6..7c4a66f9 100644 --- a/static/boot.js +++ b/static/boot.js @@ -155,6 +155,26 @@ function handleWorkspaceClose(){ closeWorkspacePanel(); } +/** + * Set a tooltip on a button, preferring the custom CSS tooltip (`data-tooltip`) + * when the element opts in via the `has-tooltip` class. Falls back to the + * native `title` attribute for elements that haven't opted in. + * + * Critical: when the element DOES have data-tooltip, this MUST also clear any + * existing native `title` attribute, otherwise the slow ~1.5s native browser + * tooltip co-fires alongside the fast custom CSS tooltip — exactly the bug + * #1775 reports. Always pair `data-tooltip` with `removeAttribute('title')`. + */ +function _setButtonTooltip(btn, text){ + if(!btn) return; + if(btn.hasAttribute('data-tooltip')){ + btn.setAttribute('data-tooltip', text); + if(btn.hasAttribute('title')) btn.removeAttribute('title'); + } else { + btn.title = text; + } +} + function syncWorkspacePanelUI(){ const {layout,panel,toggleBtn,collapseBtn}= _workspacePanelEls(); if(!layout||!panel)return; @@ -167,11 +187,11 @@ function syncWorkspacePanelUI(){ if(toggleBtn){ toggleBtn.classList.toggle('active',isOpen); toggleBtn.setAttribute('aria-pressed',isOpen?'true':'false'); - toggleBtn.title=isOpen?'Hide workspace panel':'Show workspace panel'; + _setButtonTooltip(toggleBtn, isOpen?'Hide workspace panel':'Show workspace panel'); toggleBtn.disabled=!canBrowse; } if(collapseBtn){ - collapseBtn.title=isCompact?'Close workspace panel':'Hide workspace panel'; + _setButtonTooltip(collapseBtn, isCompact?'Close workspace panel':'Hide workspace panel'); } const hasSession=!!S.session; ['btnUpDir','btnNewFile','btnNewFolder','btnRefreshPanel'].forEach(id=>{ @@ -181,7 +201,7 @@ function syncWorkspacePanelUI(){ const clearBtn=$('btnClearPreview'); if(clearBtn){ clearBtn.disabled=!isOpen; - clearBtn.title=hasPreview?'Close preview':'Hide workspace panel'; + _setButtonTooltip(clearBtn, hasPreview?'Close preview':'Hide workspace panel'); // On desktop, only show the X button when a file preview is open. // In browse mode the chevron (btnCollapseWorkspacePanel) already serves // as the close control, so showing both produces a duplicate X. @@ -278,7 +298,7 @@ $('btnAttach').onclick=()=>$('fileInput').click(); btn.classList.toggle('recording',on); // Active-state title flips so the tooltip is honest about what // pressing the button will do (#1488). - btn.title = on ? t('voice_dictate_active') : t('voice_dictate'); + _setButtonTooltip(btn, on ? t('voice_dictate_active') : t('voice_dictate')); status.style.display=on?'':'none'; if(statusText) statusText.textContent=on?'Listening':'Listening'; if(!on){ _finalText=''; _prefix=''; } @@ -702,7 +722,7 @@ window._micPendingSend=window._micPendingSend||false; function _activate(){ _voiceModeActive=true; modeBtn.classList.add('active'); - modeBtn.title=t('voice_mode_toggle_active'); + _setButtonTooltip(modeBtn, t('voice_mode_toggle_active')); showToast(t('voice_mode_active'),1500); // If the agent is busy, wait — state will be 'thinking' and we'll detect completion if(typeof S!=='undefined'&&S.busy){ @@ -719,7 +739,7 @@ window._micPendingSend=window._micPendingSend||false; _voiceModeState='idle'; _voiceModeThinkingSid=null; modeBtn.classList.remove('active'); - modeBtn.title=t('voice_mode_toggle'); + _setButtonTooltip(modeBtn, t('voice_mode_toggle')); bar.style.display='none'; clearTimeout(_silenceTimer); try{ if(_recognition) _recognition.abort(); }catch(_){} diff --git a/static/i18n.js b/static/i18n.js index e0be3a13..0728de6d 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -8741,7 +8741,17 @@ function applyLocaleToDOM() { document.querySelectorAll('[data-i18n-title]').forEach(el => { const key = el.getAttribute('data-i18n-title'); const val = t(key); - if (val && val !== key) el.title = val; + if (!val || val === key) return; + if (el.hasAttribute('data-tooltip')) { + // Custom CSS tooltip is in use (#1775) — sync it and explicitly clear + // the native `title` attribute so the slow ~1.5s browser tooltip never + // co-fires alongside the fast custom tooltip. + el.setAttribute('data-tooltip', val); + if (el.hasAttribute('title')) el.removeAttribute('title'); + } else { + // Element opted out of custom tooltips — fall back to the native title. + el.title = val; + } }); document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { const key = el.getAttribute('data-i18n-placeholder'); diff --git a/static/index.html b/static/index.html index 1561eca2..976cfa85 100644 --- a/static/index.html +++ b/static/index.html @@ -61,7 +61,7 @@