mirror of
https://github.com/block/buzz.git
synced 2026-11-04 08:21:48 +00:00
- 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
61 lines
1.8 KiB
TypeScript
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 };
|
|
}
|