From 7d1e0fe7d6caac9500b5f7099c60d6c8afb86c49 Mon Sep 17 00:00:00 2001 From: obinnanwachukwu1 Date: Thu, 20 Aug 2026 22:01:56 -0400 Subject: [PATCH 1/2] fix(web): keep sidebar stable after settings --- apps/web/src/components/AppSidebarLayout.tsx | 13 ++++++------ apps/web/src/components/Sidebar.tsx | 22 +++++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index a3ba76679689..0bc21fbe3f69 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -23,7 +23,7 @@ import { resolveSidebarStageFocusRingOffsetClass, useSidebarStageBackdropVariant, } from "./SidebarStageBackdrop"; -import { useProjects } from "../state/entities"; +import { useProjects, useThreadShells } from "../state/entities"; import { resolveInitialThreadSidebarWidth, resolveThreadSidebarMaximumWidth, @@ -128,11 +128,12 @@ function SidebarControl() { ); } -// Settings swaps the thread sidebar out of the tree. Keep the lightweight -// project projection subscribed so returning to a draft never renders the -// zero-project state while the environment snapshot reconnects. -function ProjectProjectionRetention() { +// Settings swaps the thread sidebar out of the tree. Keep its lightweight +// projections subscribed so returning never briefly renders empty project or +// thread lists while the environment snapshot reconnects. +function SidebarProjectionRetention() { useProjects(); + useThreadShells(); return null; } @@ -210,7 +211,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { return ( - + | null>(null); + const listAutoAnimateEnableFrameRef = useRef(null); const attachListAutoAnimateRef = useCallback((node: HTMLUListElement | null) => { + if (listAutoAnimateEnableFrameRef.current !== null) { + window.cancelAnimationFrame(listAutoAnimateEnableFrameRef.current); + listAutoAnimateEnableFrameRef.current = null; + } + listAutoAnimateControllerRef.current?.destroy?.(); + listAutoAnimateControllerRef.current = null; if (!node) return; - autoAnimate(node, { duration: 150, easing: "ease-out" }); + + const controller = autoAnimate(node, { duration: 150, easing: "ease-out" }); + // DndContext installs its accessibility nodes just after this list mounts. + // Treat those setup mutations as part of the initial render; otherwise + // auto-animate scales every existing thread row when Settings unmounts and + // remounts the sidebar. Real list changes after the first frame still animate. + controller.disable(); + listAutoAnimateControllerRef.current = controller; + listAutoAnimateEnableFrameRef.current = window.requestAnimationFrame(() => { + if (listAutoAnimateControllerRef.current !== controller) return; + controller.enable(); + listAutoAnimateEnableFrameRef.current = null; + }); }, []); // New thread defaults to the project you're in (active thread's project, From 1079dabb2f1206f45aff6b5ccfaa04bbc1a4f353 Mon Sep 17 00:00:00 2001 From: obinnanwachukwu1 Date: Thu, 20 Aug 2026 22:45:34 -0400 Subject: [PATCH 2/2] fix(web): preserve sidebar animation tracking --- apps/web/src/components/Sidebar.tsx | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 0c1633949c24..fb441ccef1d1 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3319,27 +3319,26 @@ export default function Sidebar() { }, [shouldShowJumpHintsNow]); const listAutoAnimateControllerRef = useRef | null>(null); - const listAutoAnimateEnableFrameRef = useRef(null); + const listAutoAnimateAttachFrameRef = useRef(null); const attachListAutoAnimateRef = useCallback((node: HTMLUListElement | null) => { - if (listAutoAnimateEnableFrameRef.current !== null) { - window.cancelAnimationFrame(listAutoAnimateEnableFrameRef.current); - listAutoAnimateEnableFrameRef.current = null; + if (listAutoAnimateAttachFrameRef.current !== null) { + window.cancelAnimationFrame(listAutoAnimateAttachFrameRef.current); + listAutoAnimateAttachFrameRef.current = null; } listAutoAnimateControllerRef.current?.destroy?.(); listAutoAnimateControllerRef.current = null; if (!node) return; - const controller = autoAnimate(node, { duration: 150, easing: "ease-out" }); // DndContext installs its accessibility nodes just after this list mounts. - // Treat those setup mutations as part of the initial render; otherwise - // auto-animate scales every existing thread row when Settings unmounts and - // remounts the sidebar. Real list changes after the first frame still animate. - controller.disable(); - listAutoAnimateControllerRef.current = controller; - listAutoAnimateEnableFrameRef.current = window.requestAnimationFrame(() => { - if (listAutoAnimateControllerRef.current !== controller) return; - controller.enable(); - listAutoAnimateEnableFrameRef.current = null; + // Attach one frame later so those setup mutations are ignored without + // canceling auto-animate's initial coordinate tracking. + listAutoAnimateAttachFrameRef.current = window.requestAnimationFrame(() => { + listAutoAnimateAttachFrameRef.current = null; + if (!node.isConnected) return; + listAutoAnimateControllerRef.current = autoAnimate(node, { + duration: 150, + easing: "ease-out", + }); }); }, []);