Files
buzz/desktop/src/shared/hooks/useStickToBottom.ts
Bradley Axen 968691806e feat(desktop): live ACP session sidebar with sticky scroll
- Add local ACP session observer (SSE hook, transcript builder, types)
- Bot activity bar with clickable agent pills to open sidebar
- Agent session thread panel with transcript list, tool items, thoughts
- Sticky scroll: auto-follows new content unless user scrolls up
2026-04-27 21:29:16 -07:00

61 lines
1.8 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react";
/**
* Keeps a scroll container pinned to the bottom as new content arrives,
* unless the user has scrolled up. Mirrors the "sticky scroll" pattern
* from goose's MessageTimeline.
*
* Attach `ref` to the scrollable container and `onScroll` as its scroll
* handler. The hook observes DOM mutations inside the container and
* auto-scrolls when the user is near the bottom (within 100 px).
*
* Scroll calls are batched via `requestAnimationFrame` so rapid streaming
* updates (e.g. token-by-token SSE) don't cause layout thrashing.
*/
export function useStickToBottom<T extends HTMLElement = HTMLDivElement>() {
const ref = useRef<T>(null);
const isNearBottomRef = useRef(true);
const onScroll = useCallback(() => {
const el = ref.current;
if (!el) return;
const { scrollTop, scrollHeight, clientHeight } = el;
isNearBottomRef.current = scrollHeight - scrollTop - clientHeight < 100;
}, []);
useEffect(() => {
const el = ref.current;
if (!el) return;
let rafId: number | null = null;
const scrollIfSticky = () => {
// Coalesce to one scroll per animation frame.
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
rafId = null;
if (isNearBottomRef.current && ref.current) {
ref.current.scrollTo({
top: ref.current.scrollHeight,
behavior: "smooth",
});
}
});
};
const observer = new MutationObserver(scrollIfSticky);
observer.observe(el, {
childList: true,
subtree: true,
characterData: true,
});
return () => {
observer.disconnect();
if (rafId !== null) cancelAnimationFrame(rafId);
};
}, []);
return { ref, onScroll, isNearBottomRef };
}