From 88726f2da44254e24afb2c244eba492dbca2a3e4 Mon Sep 17 00:00:00 2001 From: "LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:48:56 +0200 Subject: [PATCH] fix(react-ui): restore sidebar collapse in dev + stop Talk page auto-scroll (#10383) The sidebar collapse toggle silently no-op'd in dev builds. toggleCollapse ran its side effects (localStorage write + sidebar-collapse dispatch) inside the setCollapsed updater. StrictMode double-invokes updaters in dev to surface impurity, and the synchronous dispatch re-entered setState from the App/Sidebar listeners mid-update, so the toggle never committed. Production builds don't double-invoke, which is why only the dev server was affected. Compute next from current state and move the persist + broadcast into the handler body so the updater is pure. Also fix the Talk page anchoring to the transcript box on load. The transcript is its own overflow container, but scrollIntoView bubbles to every scrollable ancestor including the window, yanking the whole page down on mount. Scroll the transcript container directly instead. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto Co-authored-by: Ettore Di Giacinto --- core/http/react-ui/src/components/Sidebar.jsx | 14 ++++++++------ core/http/react-ui/src/pages/Talk.jsx | 7 +++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/http/react-ui/src/components/Sidebar.jsx b/core/http/react-ui/src/components/Sidebar.jsx index 1607c060c..58438fd51 100644 --- a/core/http/react-ui/src/components/Sidebar.jsx +++ b/core/http/react-ui/src/components/Sidebar.jsx @@ -130,12 +130,14 @@ export default function Sidebar({ isOpen, onClose }) { }, [location.pathname]) const toggleCollapse = () => { - setCollapsed(prev => { - const next = !prev - try { localStorage.setItem(COLLAPSED_KEY, String(next)) } catch (_) { /* ignore */ } - window.dispatchEvent(new CustomEvent('sidebar-collapse', { detail: { collapsed: next } })) - return next - }) + // Side effects (persist + broadcast) live in the handler body, never inside + // the setState updater: StrictMode double-invokes updaters in dev, and the + // synchronous sidebar-collapse dispatch re-entered setState from the + // listeners mid-update, so the toggle silently no-op'd in dev builds. + const next = !collapsed + try { localStorage.setItem(COLLAPSED_KEY, String(next)) } catch (_) { /* ignore */ } + setCollapsed(next) + window.dispatchEvent(new CustomEvent('sidebar-collapse', { detail: { collapsed: next } })) } const toggleSection = (id) => { diff --git a/core/http/react-ui/src/pages/Talk.jsx b/core/http/react-ui/src/pages/Talk.jsx index f8e49e102..d898bd67b 100644 --- a/core/http/react-ui/src/pages/Talk.jsx +++ b/core/http/react-ui/src/pages/Talk.jsx @@ -127,9 +127,12 @@ export default function Talk() { .finally(() => setModelsLoading(false)) }, []) - // Auto-scroll transcript + // Auto-scroll the transcript's own overflow container. scrollIntoView bubbles + // to every scrollable ancestor (incl. the window), which yanked the whole + // page down to the transcript box on mount; scoping to the box avoids it. useEffect(() => { - transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + const box = transcriptEndRef.current?.parentElement + box?.scrollTo({ top: box.scrollHeight, behavior: 'smooth' }) }, [transcript]) // Mirror Chat.jsx: connect / disconnect client MCP servers as the user toggles them.