diff --git a/package-lock.json b/package-lock.json index c4bcdac3e..f3aeef059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webbrain", - "version": "25.8.5", + "version": "25.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webbrain", - "version": "25.8.5", + "version": "25.8.6", "license": "MIT", "devDependencies": { "playwright": "^1.48.0" diff --git a/package.json b/package.json index e2b5fc734..8152e02f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webbrain", - "version": "25.8.5", + "version": "25.8.6", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "private": true, "type": "module", diff --git a/src/chrome/ARCHITECTURE.md b/src/chrome/ARCHITECTURE.md index 993c27501..339d7d66d 100644 --- a/src/chrome/ARCHITECTURE.md +++ b/src/chrome/ARCHITECTURE.md @@ -1,6 +1,6 @@ # WebBrain Chrome/Edge Extension — Architecture -> Version 25.8.5 · Manifest V3 · Service Worker background +> Version 25.8.6 · Manifest V3 · Service Worker background ## High-Level Overview diff --git a/src/chrome/manifest.json b/src/chrome/manifest.json index b9b7f9621..e0d5818b6 100644 --- a/src/chrome/manifest.json +++ b/src/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WebBrain", - "version": "25.8.5", + "version": "25.8.6", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "permissions": [ "sidePanel", diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index 33d3111d5..f788e950f 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -47,7 +47,7 @@ import { ADDITIONAL_PROVIDER_UI } from '../providers/provider-catalog.js'; // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '25.8.5'; +const EXT_VERSION = '25.8.6'; const providersContainer = document.getElementById('providers'); const displaySettings = document.getElementById('display-settings'); diff --git a/src/chrome/src/ui/sidepanel.js b/src/chrome/src/ui/sidepanel.js index e846a8f50..780e34210 100644 --- a/src/chrome/src/ui/sidepanel.js +++ b/src/chrome/src/ui/sidepanel.js @@ -523,6 +523,14 @@ const activityText = document.getElementById('activity-text'); const modeAskBtn = document.getElementById('btn-mode-ask'); const modeActBtn = document.getElementById('btn-mode-act'); const modeDevBtn = document.getElementById('btn-mode-dev'); +const modeToggleEl = document.getElementById('mode-toggle'); +const modeToggleHighlight = (() => { + const el = document.createElement('div'); + el.className = 'mode-toggle-highlight instant'; + el.setAttribute('aria-hidden', 'true'); + modeToggleEl?.prepend(el); + return el; +})(); const actWarning = document.getElementById('act-warning'); const inputArea = document.getElementById('input-area'); const slashCommandMenuEl = document.getElementById('slash-command-menu'); @@ -9503,6 +9511,14 @@ async function handleGlobalKeydown(e) { // --- Mode Toggle --- +function positionModeHighlight(btn, { instant = false } = {}) { + if (!modeToggleHighlight || !btn) return; + if (instant) modeToggleHighlight.classList.add('instant'); + modeToggleHighlight.style.width = `${btn.offsetWidth}px`; + modeToggleHighlight.style.transform = `translate3d(${btn.offsetLeft}px, 0, 0)`; + if (instant) requestAnimationFrame(() => modeToggleHighlight.classList.remove('instant')); +} + function setMode(mode) { if (mode !== 'ask' && mode !== 'act' && mode !== 'dev') mode = 'ask'; const previousMode = agentMode; @@ -9521,6 +9537,12 @@ function setMode(mode) { modeDevBtn?.classList.toggle('active', mode === 'dev'); modeDevBtn?.classList.toggle('act', mode === 'dev'); inputArea.classList.toggle('act-mode', mode !== 'ask'); + + // Slide the highlight pill to the active button + const activeBtn = mode === 'ask' ? modeAskBtn : mode === 'act' ? modeActBtn : modeDevBtn; + modeToggleHighlight?.classList.toggle('act', mode !== 'ask'); + positionModeHighlight(activeBtn); + updateActWarning(); resetInputPlaceholderRotation(); } @@ -9559,6 +9581,24 @@ modeDevBtn?.addEventListener('click', async () => { await ensureDevMode(); }); +// Set initial highlight position without animation +requestAnimationFrame(() => positionModeHighlight(modeAskBtn, { instant: true })); + +// Recalculate highlight when the toggle resizes (e.g. side panel drag) +if (modeToggleEl) { + new ResizeObserver(() => { + const activeBtn = modeToggleEl.querySelector('.mode-btn.active'); + if (activeBtn) positionModeHighlight(activeBtn, { instant: true }); + }).observe(modeToggleEl); +} + +// Reposition highlight when document direction changes (RTL locale switch) +new MutationObserver(() => { + requestAnimationFrame(() => { + const activeBtn = modeToggleEl?.querySelector('.mode-btn.active'); + if (activeBtn) positionModeHighlight(activeBtn, { instant: true }); + }); +}).observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] }); // --- Stop / Abort --- diff --git a/src/chrome/styles/sidepanel.css b/src/chrome/styles/sidepanel.css index 9f0026b49..518e821a3 100644 --- a/src/chrome/styles/sidepanel.css +++ b/src/chrome/styles/sidepanel.css @@ -2112,10 +2112,37 @@ body { border: 1px solid var(--border); border-radius: 8px; padding: 2px; + position: relative; +} + +.mode-toggle-highlight { + position: absolute; + top: 2px; + left: 0; + z-index: 0; + height: calc(100% - 4px); + border-radius: 6px; + background: var(--accent-dim); + pointer-events: none; + will-change: transform, width; + transition: + transform 170ms cubic-bezier(0.22, 1, 0.36, 1), + width 170ms cubic-bezier(0.22, 1, 0.36, 1), + background 170ms ease; +} + +.mode-toggle-highlight.act { + background: rgba(255, 152, 0, 0.12); +} + +.mode-toggle-highlight.instant { + transition: none; } .mode-btn { flex: 1; + position: relative; + z-index: 1; background: none; border: none; color: var(--text-muted); @@ -2124,7 +2151,7 @@ body { padding: 5px 0; border-radius: 6px; cursor: pointer; - transition: all 0.2s; + transition: color 0.2s; text-align: center; } @@ -2133,12 +2160,10 @@ body { } .mode-btn.active { - background: var(--accent-dim); color: var(--accent); } .mode-btn.active.act { - background: rgba(255, 152, 0, 0.12); color: var(--warning); } diff --git a/src/firefox/ARCHITECTURE.md b/src/firefox/ARCHITECTURE.md index df3cb1639..fb07f4d43 100644 --- a/src/firefox/ARCHITECTURE.md +++ b/src/firefox/ARCHITECTURE.md @@ -1,6 +1,6 @@ # WebBrain Firefox Extension — Architecture -> Version 25.8.5 · Manifest V2 · Background Page +> Version 25.8.6 · Manifest V2 · Background Page ## How Firefox Differs from Chrome diff --git a/src/firefox/manifest.json b/src/firefox/manifest.json index c865be5b8..a0041e65a 100644 --- a/src/firefox/manifest.json +++ b/src/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "WebBrain", - "version": "25.8.5", + "version": "25.8.6", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "permissions": [ "activeTab", diff --git a/src/firefox/src/ui/settings.js b/src/firefox/src/ui/settings.js index 87eedbab7..3aebfcdc3 100644 --- a/src/firefox/src/ui/settings.js +++ b/src/firefox/src/ui/settings.js @@ -47,7 +47,7 @@ import { ADDITIONAL_PROVIDER_UI } from '../providers/provider-catalog.js'; // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '25.8.5'; +const EXT_VERSION = '25.8.6'; const providersContainer = document.getElementById('providers'); const displaySettings = document.getElementById('display-settings'); diff --git a/src/firefox/src/ui/sidepanel.js b/src/firefox/src/ui/sidepanel.js index 25c14a97e..3d3bf63a8 100644 --- a/src/firefox/src/ui/sidepanel.js +++ b/src/firefox/src/ui/sidepanel.js @@ -398,6 +398,14 @@ const activityText = document.getElementById('activity-text'); const modeAskBtn = document.getElementById('btn-mode-ask'); const modeActBtn = document.getElementById('btn-mode-act'); const modeDevBtn = document.getElementById('btn-mode-dev'); +const modeToggleEl = document.getElementById('mode-toggle'); +const modeToggleHighlight = (() => { + const el = document.createElement('div'); + el.className = 'mode-toggle-highlight instant'; + el.setAttribute('aria-hidden', 'true'); + modeToggleEl?.prepend(el); + return el; +})(); const actWarning = document.getElementById('act-warning'); const inputArea = document.getElementById('input-area'); const slashCommandMenuEl = document.getElementById('slash-command-menu'); @@ -9073,6 +9081,14 @@ async function sendToBackground(action, data = {}) { // --- Mode Toggle --- +function positionModeHighlight(btn, { instant = false } = {}) { + if (!modeToggleHighlight || !btn) return; + if (instant) modeToggleHighlight.classList.add('instant'); + modeToggleHighlight.style.width = `${btn.offsetWidth}px`; + modeToggleHighlight.style.transform = `translate3d(${btn.offsetLeft}px, 0, 0)`; + if (instant) requestAnimationFrame(() => modeToggleHighlight.classList.remove('instant')); +} + function setMode(mode) { if (mode !== 'ask' && mode !== 'act' && mode !== 'dev') mode = 'ask'; agentMode = mode; @@ -9084,6 +9100,12 @@ function setMode(mode) { modeDevBtn?.classList.toggle('active', mode === 'dev'); modeDevBtn?.classList.toggle('act', mode === 'dev'); inputArea.classList.toggle('act-mode', mode !== 'ask'); + + // Slide the highlight pill to the active button + const activeBtn = mode === 'ask' ? modeAskBtn : mode === 'act' ? modeActBtn : modeDevBtn; + modeToggleHighlight?.classList.toggle('act', mode !== 'ask'); + positionModeHighlight(activeBtn); + updateActWarning(); resetInputPlaceholderRotation(); } @@ -9122,6 +9144,24 @@ modeDevBtn?.addEventListener('click', async () => { await ensureDevMode(); }); +// Set initial highlight position without animation +requestAnimationFrame(() => positionModeHighlight(modeAskBtn, { instant: true })); + +// Recalculate highlight when the toggle resizes (e.g. side panel drag) +if (modeToggleEl) { + new ResizeObserver(() => { + const activeBtn = modeToggleEl.querySelector('.mode-btn.active'); + if (activeBtn) positionModeHighlight(activeBtn, { instant: true }); + }).observe(modeToggleEl); +} + +// Reposition highlight when document direction changes (RTL locale switch) +new MutationObserver(() => { + requestAnimationFrame(() => { + const activeBtn = modeToggleEl?.querySelector('.mode-btn.active'); + if (activeBtn) positionModeHighlight(activeBtn, { instant: true }); + }); +}).observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] }); // --- Stop / Abort --- diff --git a/src/firefox/styles/sidepanel.css b/src/firefox/styles/sidepanel.css index 79de06e44..a11e2c131 100644 --- a/src/firefox/styles/sidepanel.css +++ b/src/firefox/styles/sidepanel.css @@ -1983,10 +1983,37 @@ body { border: 1px solid var(--border); border-radius: 8px; padding: 2px; + position: relative; +} + +.mode-toggle-highlight { + position: absolute; + top: 2px; + left: 0; + z-index: 0; + height: calc(100% - 4px); + border-radius: 6px; + background: var(--accent-dim); + pointer-events: none; + will-change: transform, width; + transition: + transform 170ms cubic-bezier(0.22, 1, 0.36, 1), + width 170ms cubic-bezier(0.22, 1, 0.36, 1), + background 170ms ease; +} + +.mode-toggle-highlight.act { + background: rgba(255, 152, 0, 0.12); +} + +.mode-toggle-highlight.instant { + transition: none; } .mode-btn { flex: 1; + position: relative; + z-index: 1; background: none; border: none; color: var(--text-muted); @@ -1995,7 +2022,7 @@ body { padding: 5px 0; border-radius: 6px; cursor: pointer; - transition: all 0.2s; + transition: color 0.2s; text-align: center; } @@ -2004,12 +2031,10 @@ body { } .mode-btn.active { - background: var(--accent-dim); color: var(--accent); } .mode-btn.active.act { - background: rgba(255, 152, 0, 0.12); color: var(--warning); }