From ffc26b7fb1af5317178cf265ec0a600b65c56e2b Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Wed, 17 Jun 2026 22:48:03 +0000 Subject: [PATCH] fix(react-ui): restore sidebar collapse in dev + stop Talk page auto-scroll 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. Signed-off-by: Ettore Di Giacinto Assisted-by: Claude:claude-opus-4-8 [Claude Code] --- 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 1607c060c263..58438fd5184d 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 f8e49e102677..d898bd67ba71 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.