buddy scroll

This commit is contained in:
2026-09-25 12:31:57 +05:30
parent 46b73c50f4
commit 72f3bb0961

View File

@@ -350,6 +350,7 @@ export function AssistantPanel({
const [isFocused, setIsFocused] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const threadRef = useRef<HTMLDivElement>(null);
const { width, setWidth, reset } = useAssistantWidth();
const [isDragging, setDragging] = useState(false);
@@ -405,6 +406,30 @@ export function AssistantPanel({
setThread([]);
}, [key]);
/*
* Follow the conversation down, unless the person has scrolled away from it.
*
* A chat that does not follow leaves the newest answer below the fold, which
* reads as nothing having happened. One that follows unconditionally yanks
* somebody out of an earlier answer they were still reading the moment a
* reply lands — and a reply can land a while after the question, because a
* tool call and a model round trip are seconds, not milliseconds.
*
* So: only when they were already at the bottom. The 40px allowance covers
* fractional scroll heights, which browsers disagree about by a pixel or two
* at non-integer zoom levels — without it this silently stops following for
* anybody not at 100%.
*/
useEffect(() => {
const el = threadRef.current;
if (!el) return;
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
if (distanceFromBottom > 40) return;
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
}, [thread]);
/*
* Approving is its own call, with no question in it.
*
@@ -569,6 +594,35 @@ export function AssistantPanel({
)}
</div>
{/*
The scrolling part, and the only one.
Everything above this is chrome that stays put; everything below it is
the composer, which must never leave the screen. Before this existed
the panel was one `height: 100%` column with no overflow anywhere: a
thread longer than the panel pushed the chips and the composer past
the bottom edge, and there was nothing to scroll — the box you type
into simply left the screen and the conversation could not be read
back.
`minHeight: 0` is the load-bearing half. A flex child defaults to
`min-height: auto`, which means it refuses to shrink below its content,
so `flex: 1` alone would let this grow to the height of the whole
conversation and overflow the panel exactly as before — the scrollbar
never appears and nothing looks wrong in the code.
*/}
<div
ref={threadRef}
style={{
flex: 1,
minHeight: 0,
overflowY: 'auto',
overscrollBehavior: 'contain',
display: 'flex',
flexDirection: 'column',
gap: 16,
}}
>
{/* Greeting — top-aligned, because it is the first thing in a
conversation, not a splash screen. */}
<div>
@@ -608,9 +662,9 @@ export function AssistantPanel({
</p>
</div>
{/* The conversation. Above the spacer, so a short thread sits under the
greeting and a long one pushes the chips and composer down rather
than scrolling away from them. */}
{/* The conversation. Inside the scroller with the greeting above it, so
a short thread reads straight on from the greeting and a long one
scrolls under it — with the chips and the composer staying put. */}
{thread.length > 0 ? (
<div
role="log"
@@ -629,8 +683,7 @@ export function AssistantPanel({
))}
</div>
) : null}
<div style={{ flex: 1 }} />
</div>
{/* Chips wrap, never scroll — a half-visible button at a scroller's edge
is a bug no amount of fade masking fixes. */}