diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index 4c8515246c85..8d180d6a64aa 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -198,6 +198,7 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { useIsMobile } from "~/hooks/useMediaQuery"; import { CommandDialogTrigger } from "./ui/command"; import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; +import { usePanelAnimationSettings } from "~/panelAnimations"; import { primaryServerKeybindingsAtom } from "../state/server"; import { derivePhysicalProjectKey, @@ -3341,23 +3342,25 @@ export default function LegacySidebar() { dragInProgressRef.current = false; }, []); - const animatedProjectListsRef = useRef(new WeakSet()); - const attachProjectListAutoAnimateRef = useCallback((node: HTMLElement | null) => { - if (!node || animatedProjectListsRef.current.has(node)) { - return; - } - autoAnimate(node, SIDEBAR_LIST_ANIMATION_OPTIONS); - animatedProjectListsRef.current.add(node); - }, []); - - const animatedThreadListsRef = useRef(new WeakSet()); - const attachThreadListAutoAnimateRef = useCallback((node: HTMLElement | null) => { - if (!node || animatedThreadListsRef.current.has(node)) { - return; - } - autoAnimate(node, SIDEBAR_LIST_ANIMATION_OPTIONS); - animatedThreadListsRef.current.add(node); - }, []); + // Both lists follow the Motion setting, as the default sidebar does: at the + // default 0 ms they change immediately and auto-animate is never attached, + // so its per-row position polling (#4693) never runs. A detached list is + // destroyed rather than left polling. + const { active: listMotionActive, durationMs: listMotionDurationMs } = + usePanelAnimationSettings(); + const attachListAutoAnimateRef = useCallback( + (node: HTMLElement | null) => { + if (!node || !listMotionActive) return; + const controller = autoAnimate(node, { + ...SIDEBAR_LIST_ANIMATION_OPTIONS, + duration: Math.min(SIDEBAR_LIST_ANIMATION_OPTIONS.duration, listMotionDurationMs), + }); + return () => controller.destroy?.(); + }, + [listMotionActive, listMotionDurationMs], + ); + const attachProjectListAutoAnimateRef = attachListAutoAnimateRef; + const attachThreadListAutoAnimateRef = attachListAutoAnimateRef; const visibleThreads = useMemo( () => sidebarThreads.filter((thread) => thread.archivedAt === null), diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 38a26c7ac4e2..b4307ab0a8b5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -107,6 +107,7 @@ import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { openCommandPalette } from "../commandPaletteBus"; import { startNewThreadFromContext } from "../lib/chatThreadActions"; import { useClientSettings } from "../hooks/useSettings"; +import { usePanelAnimationSettings } from "../panelAnimations"; import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; import { useLocalStorage } from "../hooks/useLocalStorage"; import { useNowMinute } from "../hooks/useNowMinute"; @@ -3458,10 +3459,25 @@ export default function Sidebar() { updateThreadJumpHintsVisibility(shouldShowJumpHintsNow); }, [shouldShowJumpHintsNow, updateThreadJumpHintsVisibility]); - const attachListAutoAnimateRef = useCallback((node: HTMLUListElement | null) => { - if (!node) return; - autoAnimate(node, { duration: 150, easing: "ease-out" }); - }, []); + // The thread list follows the same Motion setting as the panels: at the + // default 0 ms a row appears, moves, or leaves immediately. auto-animate + // also polls every row's position for as long as it is attached, which alone + // kept an idle app on a quarter of a core (#4693), so a list that never + // animates is never attached, and a detached list is destroyed rather than + // left polling. + const { active: listMotionActive, durationMs: listMotionDurationMs } = + usePanelAnimationSettings(); + const attachListAutoAnimateRef = useCallback( + (node: HTMLUListElement | null) => { + if (!node || !listMotionActive) return; + const controller = autoAnimate(node, { + duration: Math.min(150, listMotionDurationMs), + easing: "ease-out", + }); + return () => controller.destroy?.(); + }, + [listMotionActive, listMotionDurationMs], + ); // New thread defaults to the project you're in (active thread's project, // falling back to the top project) — same resolution the command palette diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 204678eb1d2f..2c32024282ff 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -55,6 +55,10 @@ The main sidebar, right panel, and terminal drawer open and close immediately by The duration can be set up to 400 ms. Clicking the preview replays all three panel transitions; at 0 ms, it snaps between the same open and closed states. +Thread rows in the sidebar follow the same setting. At 0 ms, or when your system asks for reduced +motion, a thread appears, moves between sections, or leaves the list immediately; otherwise it +slides into place. + ## Environment icons When you are connected to more than one environment, every thread that lives somewhere other than